From 0ed0b0a2185416e053a64f08fb39a7b7566bd4f6 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 9 Apr 2025 10:27:00 +0200 Subject: [PATCH] finishes install + uninstall --- src/__mocks__/handler.mocks.js | 8 ++ src/__mocks__/service.mocks.js | 4 + src/handlers/installer.js | 7 +- src/handlers/installer.test.js | 24 +++++- src/main.js | 14 +++- src/services/fsService.js | 4 + src/services/uiService.js | 2 +- src/ui/installMenu.js | 58 ++++++++++----- src/ui/installMenu.test.js | 130 ++++++++++++++++++++++++++++++++- src/utils/pathSelector.js | 3 +- src/utils/pathSelector.test.js | 8 +- 11 files changed, 232 insertions(+), 30 deletions(-) create mode 100644 src/__mocks__/handler.mocks.js diff --git a/src/__mocks__/handler.mocks.js b/src/__mocks__/handler.mocks.js new file mode 100644 index 0000000..5b8f8df --- /dev/null +++ b/src/__mocks__/handler.mocks.js @@ -0,0 +1,8 @@ +import { vi } from "vitest"; + +export const mockInstaller = { + isCodexInstalled: vi.fn(), + getCodexVersion: vi.fn(), + installCodex: vi.fn(), + uninstallCodex: vi.fn(), +}; diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 5b735f2..3610f8e 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -6,6 +6,9 @@ export const mockUiService = { showErrorMessage: vi.fn(), askMultipleChoice: vi.fn(), askPrompt: vi.fn(), + createAndStartSpinner: vi.fn(), + stopSpinnerSuccess: vi.fn(), + stopSpinnerError: vi.fn(), }; export const mockConfigService = { @@ -21,6 +24,7 @@ export const mockFsService = { isFile: vi.fn(), readDir: vi.fn(), makeDir: vi.fn(), + deleteDir: vi.fn(), }; export const mockShellService = { diff --git a/src/handlers/installer.js b/src/handlers/installer.js index 740e8b7..ca0eb4f 100644 --- a/src/handlers/installer.js +++ b/src/handlers/installer.js @@ -39,6 +39,11 @@ export class Installer { processCallbacks.installSuccessful(); }; + uninstallCodex = () => { + this.fs.deleteDir(this.config.codexInstallPath); + this.fs.deleteDir(this.config.dataDir); + }; + arePrerequisitesCorrect = async (processCallbacks) => { if (await this.isCodexInstalled()) { processCallbacks.warn("Codex is already installed."); @@ -74,7 +79,7 @@ export class Installer { }; installCodexUnix = async (processCallbacks) => { - if (!await this.ensureUnixDependencies(processCallbacks)) return; + if (!(await this.ensureUnixDependencies(processCallbacks))) return; await this.shell.run( "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh", ); diff --git a/src/handlers/installer.test.js b/src/handlers/installer.test.js index 72cd207..3eea1cf 100644 --- a/src/handlers/installer.test.js +++ b/src/handlers/installer.test.js @@ -241,7 +241,9 @@ describe("Installer", () => { it("ensures unix dependencies", async () => { await installer.installCodexUnix(processCallbacks); - expect(installer.ensureUnixDependencies).toHaveBeenCalled(processCallbacks); + expect(installer.ensureUnixDependencies).toHaveBeenCalled( + processCallbacks, + ); }); it("returns early if unix dependencies are not met", async () => { @@ -255,9 +257,9 @@ describe("Installer", () => { }); describe("when dependencies are met", () => { - beforeEach(() =>{ + beforeEach(() => { installer.ensureUnixDependencies.mockResolvedValue(true); - }) + }); it("runs the curl command to download the installer", async () => { await installer.installCodexUnix(processCallbacks); @@ -385,4 +387,20 @@ describe("Installer", () => { expect(mockConfigService.saveConfig).toHaveBeenCalled(); }); }); + + describe("uninstallCodex", () => { + it("deletes the codex install path", () => { + installer.uninstallCodex(); + + expect(mockFsService.deleteDir).toHaveBeenCalledWith( + config.codexInstallPath, + ); + }); + + it("deletes the codex data path", () => { + installer.uninstallCodex(); + + expect(mockFsService.deleteDir).toHaveBeenCalledWith(config.dataDir); + }); + }); }); diff --git a/src/main.js b/src/main.js index c3240ea..b37b231 100644 --- a/src/main.js +++ b/src/main.js @@ -105,8 +105,18 @@ export async function main() { const numberSelector = new NumberSelector(uiService); const shellService = new ShellService(); const osService = new OsService(); - const installer = new Installer(configService, shellService, osService, fsService); - const installMenu = new InstallMenu(uiService, configService, pathSelector, installer); + const installer = new Installer( + configService, + shellService, + osService, + fsService, + ); + const installMenu = new InstallMenu( + uiService, + configService, + pathSelector, + installer, + ); const configMenu = new ConfigMenu( uiService, new MenuLoop(), diff --git a/src/services/fsService.js b/src/services/fsService.js index 8590652..fee2b05 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -50,4 +50,8 @@ export class FsService { moveDir = (oldPath, newPath) => { fs.moveSync(oldPath, newPath); }; + + deleteDir = (dir) => { + fs.rmSync(dir, { recursive: true, force: true }); + }; } diff --git a/src/services/uiService.js b/src/services/uiService.js index 6c4d2af..1ce5955 100644 --- a/src/services/uiService.js +++ b/src/services/uiService.js @@ -91,7 +91,7 @@ export class UiService { createAndStartSpinner = (message) => { return createSpinner(message).start(); - } + }; stopSpinnerSuccess = (spinner) => { if (spinner == undefined) return; diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index 2008fb8..6ce3053 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -13,20 +13,7 @@ export class InstallMenu { } else { await this.showInstallMenu(); } - } - - showUninstallMenu = async () => { - await this.ui.askMultipleChoice("Codex is installed", [ - { - label: "Uninstall", - action: this.performUninstall, - }, - { - label: "Cancel", - action: this.doNothing, - }, - ]); - } + }; showInstallMenu = async () => { await this.ui.askMultipleChoice("Configure your Codex installation", [ @@ -49,6 +36,41 @@ export class InstallMenu { ]); }; + showUninstallMenu = async () => { + await this.ui.askMultipleChoice("Codex is installed", [ + { + label: "Uninstall", + action: this.showConfirmUninstall, + }, + { + label: "Cancel", + action: this.doNothing, + }, + ]); + }; + + showConfirmUninstall = async () => { + this.ui.showInfoMessage( + "You are about to:\n" + + " - Uninstall the Codex application\n" + + " - Delete the data stored in your Codex node", + ); + + await this.ui.askMultipleChoice( + "Are you sure you want to uninstall Codex?", + [ + { + label: "No", + action: this.doNothing, + }, + { + label: "Yes", + action: this.performUninstall, + }, + ], + ); + }; + selectInstallPath = async () => { this.config.codexInstallPath = await this.pathSelector.show( this.config.codexInstallPath, @@ -66,7 +88,9 @@ export class InstallMenu { await this.installer.installCodex(this); }; - performUninstall = async () => {}; + performUninstall = async () => { + this.installer.uninstallCodex(); + }; doNothing = async () => {}; @@ -77,12 +101,12 @@ export class InstallMenu { downloadSuccessful = () => { this.ui.showInfoMessage("Download successful..."); - } + }; installSuccessful = () => { this.ui.showInfoMessage("Installation successful!"); this.ui.stopSpinnerSuccess(this.installSpinner); - } + }; warn = (message) => { this.ui.showErrorMessage(message); diff --git a/src/ui/installMenu.test.js b/src/ui/installMenu.test.js index 2e15e89..1b47cd4 100644 --- a/src/ui/installMenu.test.js +++ b/src/ui/installMenu.test.js @@ -3,6 +3,7 @@ import { InstallMenu } from "./installMenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockConfigService } from "../__mocks__/service.mocks.js"; import { mockPathSelector } from "../__mocks__/utils.mocks.js"; +import { mockInstaller } from "../__mocks__/handler.mocks.js"; describe("InstallMenu", () => { const config = { @@ -18,11 +19,35 @@ describe("InstallMenu", () => { mockUiService, mockConfigService, mockPathSelector, + mockInstaller, ); }); + describe("show", () => { + beforeEach(() => { + installMenu.showInstallMenu = vi.fn(); + installMenu.showUninstallMenu = vi.fn(); + }); + + it("shows uninstall menu when codex is installed", async () => { + mockInstaller.isCodexInstalled.mockResolvedValue(true); + + await installMenu.show(); + + expect(installMenu.showUninstallMenu).toHaveBeenCalled(); + }); + + it("shows install menu when codex is not installed", async () => { + mockInstaller.uninstallCodex.mockResolvedValue(false); + + await installMenu.show(); + + expect(installMenu.showInstallMenu).toHaveBeenCalled(); + }); + }); + it("displays the install menu", async () => { - await installMenu.show(); + await installMenu.showInstallMenu(); expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( "Configure your Codex installation", [ @@ -46,6 +71,47 @@ describe("InstallMenu", () => { ); }); + it("displays the uninstall menu", async () => { + await installMenu.showUninstallMenu(); + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Codex is installed", + [ + { + label: "Uninstall", + action: installMenu.showConfirmUninstall, + }, + { + label: "Cancel", + action: installMenu.doNothing, + }, + ], + ); + }); + + it("confirms uninstall", async () => { + await installMenu.showConfirmUninstall(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "You are about to:\n" + + " - Uninstall the Codex application\n" + + " - Delete the data stored in your Codex node", + ); + + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Are you sure you want to uninstall Codex?", + [ + { + label: "No", + action: installMenu.doNothing, + }, + { + label: "Yes", + action: installMenu.performUninstall, + }, + ], + ); + }); + it("allows selecting the install path", async () => { const originalPath = config.codexInstallPath; const newPath = "/new/path"; @@ -68,4 +134,66 @@ describe("InstallMenu", () => { "This option is not currently available.", ); }); + + it("calls installed for installation", async () => { + await installMenu.performInstall(); + + expect(mockInstaller.installCodex).toHaveBeenCalledWith(installMenu); + }); + + it("calls installer for deinstallation", async () => { + await installMenu.performUninstall(); + + expect(mockInstaller.uninstallCodex).toHaveBeenCalled(); + }); + + describe("process callback handling", () => { + const mockSpinner = { + isRealSpinner: "no srry", + }; + + beforeEach(() => { + mockUiService.createAndStartSpinner.mockReturnValue(mockSpinner); + }); + + it("creates spinner on installStarts", () => { + installMenu.installStarts(); + + expect(installMenu.installSpinner).toBe(mockSpinner); + expect(mockUiService.createAndStartSpinner).toHaveBeenCalledWith( + "Installing...", + ); + }); + + it("shows download success message", () => { + installMenu.downloadSuccessful(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Download successful...", + ); + }); + + it("shows install success message", () => { + installMenu.installSpinner = mockSpinner; + + installMenu.installSuccessful(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Installation successful!", + ); + expect(mockUiService.stopSpinnerSuccess).toHaveBeenCalledWith( + mockSpinner, + ); + }); + + it("shows warnings", () => { + const message = "warning!"; + installMenu.installSpinner = mockSpinner; + + installMenu.warn(message); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(message); + expect(mockUiService.stopSpinnerError).toHaveBeenCalledWith(mockSpinner); + }); + }); }); diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index f75eba9..742c8bb 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -143,8 +143,7 @@ export class PathSelector { try { const entries = this.fs.readDir(fullPath); return entries.filter((entry) => this.isSubDir(entry)); - } - catch { + } catch { return []; } }; diff --git a/src/utils/pathSelector.test.js b/src/utils/pathSelector.test.js index 1cfebb1..6635cc4 100644 --- a/src/utils/pathSelector.test.js +++ b/src/utils/pathSelector.test.js @@ -102,12 +102,14 @@ describe("PathSelector", () => { }); it("handles non-existing paths", async () => { - mockFsService.readDir.mockImplementationOnce(() => { throw new Error("A!"); }); - + mockFsService.readDir.mockImplementationOnce(() => { + throw new Error("A!"); + }); + await pathSelector.downOne(); expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( - "There are no subdirectories here." + "There are no subdirectories here.", ); });