covers configmenu with tests

This commit is contained in:
ThatBen 2025-04-01 14:33:44 +02:00
parent 1f694f7534
commit 36395b62ec
No known key found for this signature in database
GPG Key ID: E020A7DDCD52E1AB
12 changed files with 378 additions and 248 deletions

View File

@ -5,7 +5,7 @@ export const mockUiService = {
showInfoMessage: vi.fn(),
showErrorMessage: vi.fn(),
askMultipleChoice: vi.fn(),
askPrompt: vi.fn()
askPrompt: vi.fn(),
};
export const mockConfigService = {

View File

@ -1,9 +1,9 @@
import { vi } from "vitest";
export const mockInstallMenu = {
show: vi.fn()
show: vi.fn(),
};
export const mockConfigMenu = {
show: vi.fn()
show: vi.fn(),
};

View File

@ -99,7 +99,12 @@ export async function main() {
const pathSelector = new PathSelector(uiService, fsService);
const numberSelector = new NumberSelector(uiService);
const installMenu = new InstallMenu(uiService, configService);
const configMenu = new ConfigMenu(uiService, configService, pathSelector, numberSelector)
const configMenu = new ConfigMenu(
uiService,
configService,
pathSelector,
numberSelector,
);
const mainMenu = new MainMenu(uiService, installMenu, configMenu);
await mainMenu.show();

View File

@ -28,7 +28,7 @@ export class ConfigService {
get = () => {
return this.config;
}
};
loadConfig = () => {
const filePath = this.getConfigFilename();
@ -44,7 +44,7 @@ export class ConfigService {
);
throw error;
}
}
};
saveConfig = () => {
const filePath = this.getConfigFilename();
@ -56,9 +56,9 @@ export class ConfigService {
);
throw error;
}
}
};
getConfigFilename = () => {
return path.join(getAppDataDir(), "config.json");
}
};
}

View File

@ -1,54 +1,66 @@
export class ConfigMenu {
constructor(uiService, configService, pathSelector, numberSelector) {
constructor(
uiService,
menuLoop,
configService,
pathSelector,
numberSelector,
) {
this.ui = uiService;
this.loop = menuLoop;
this.configService = configService;
this.pathSelector = pathSelector;
this.numberSelector = numberSelector;
this.loop.initialize(this.showConfigMenu);
}
show = async() => {
this.running = true;
show = async () => {
this.config = this.configService.get();
while (this.running) {
this.ui.showInfoMessage("Codex Configuration");
await this.ui.askMultipleChoice("Select to edit:",[
{
label: `Data path = "${this.config.dataDir}"`,
action: this.editDataDir,
},
{
label: `Logs path = "${this.config.logsDir}"`,
action: this.editLogsDir,
},
{
label: `Storage quota = ${this.bytesAmountToString(this.config.storageQuota)}`,
action: this.editStorageQuota,
},
{
label: `Discovery port = ${this.config.ports.discPort}`,
action: this.editDiscPort,
},
{
label: `P2P listen port = ${this.config.ports.listenPort}`,
action: this.editListenPort,
},
{
label: `API port = ${this.config.ports.apiPort}`,
action: this.editApiPort,
},
{
label: "Save changes and exit",
action: this.saveChangesAndExit,
},
{
label: "Discard changes and exit",
action: this.discardChangesAndExit,
}
]
)
}
}
this.ui.showInfoMessage("Codex Configuration");
await this.loop.showLoop();
};
showConfigMenu = async () => {
await this.ui.askMultipleChoice("Select to edit:", [
{
label: `Data path = "${this.config.dataDir}"`,
action: this.editDataDir,
},
{
label: `Logs path = "${this.config.logsDir}"`,
action: this.editLogsDir,
},
{
label: `Storage quota = ${this.bytesAmountToString(this.config.storageQuota)}`,
action: this.editStorageQuota,
},
{
label: `Discovery port = ${this.config.ports.discPort}`,
action: this.editDiscPort,
},
{
label: `P2P listen port = ${this.config.ports.listenPort}`,
action: this.editListenPort,
},
{
label: `API port = ${this.config.ports.apiPort}`,
action: this.editApiPort,
},
{
label: "Save changes and exit",
action: this.saveChangesAndExit,
},
{
label: "Discard changes and exit",
action: this.discardChangesAndExit,
},
]);
};
// this and the byte-format handling in
// numberSelector should be extracted to
// their own util.
bytesAmountToString = (numBytes) => {
const units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
@ -61,18 +73,16 @@ export class ConfigMenu {
if (index == 0) return `${numBytes} Bytes`;
return `${numBytes} Bytes (${value} ${units[index]})`;
}
};
editDataDir = async () => {
// todo
// function updateDataDir(config, newDataDir) {
// if (config.dataDir == newDataDir) return config;
// // The Codex dataDir is a little strange:
// // If the old one is empty: The new one should not exist, so that codex creates it
// // with the correct security permissions.
// // If the old one does exist: We move it.
// if (isDir(config.dataDir)) {
// console.log(
// showInfoMessage(
@ -81,7 +91,6 @@ export class ConfigMenu {
// `To: "${newDataDir}"`,
// ),
// );
// try {
// fs.moveSync(config.dataDir, newDataDir);
// } catch (error) {
@ -104,64 +113,82 @@ export class ConfigMenu {
// );
// }
// }
// config.dataDir = newDataDir;
// return config;
// }
}
};
editLogsDir = async () => {
this.config.logsDir = await this.pathSelector.show(this.config.logsDir, true);
}
this.config.logsDir = await this.pathSelector.show(
this.config.logsDir,
true,
);
};
editStorageQuota = async () => {
this.ui.showInfoMessage("You can use: 'GB' or 'gb', etc.");
const newQuota = await this.numberSelector.show(this.config.storageQuota, "Storage quota", true);
const newQuota = await this.numberSelector.show(
this.config.storageQuota,
"Storage quota",
true,
);
if (newQuota < 100 * 1024 * 1024) {
this.ui.showErrorMessage("Storage quote should be >= 100mb.");
} else {
this.config.storageQuota = newQuota;
}
}
};
editDiscPort = async () => {
const newPort = await this.numberSelector.show(this.config.ports.discPort, "Discovery port", false);
const newPort = await this.numberSelector.show(
this.config.ports.discPort,
"Discovery port",
false,
);
if (this.isInPortRange(newPort)) {
this.config.ports.discPort = newPort;
}
}
};
editListenPort = async () => {
const newPort = await this.numberSelector.show(this.config.ports.listenPort, "P2P listen port", false);
const newPort = await this.numberSelector.show(
this.config.ports.listenPort,
"P2P listen port",
false,
);
if (this.isInPortRange(newPort)) {
this.config.ports.listenPort = newPort;
}
}
};
editApiPort = async () => {
const newPort = await this.numberSelector.show(this.config.ports.apiPort, "API port", false);
const newPort = await this.numberSelector.show(
this.config.ports.apiPort,
"API port",
false,
);
if (this.isInPortRange(newPort)) {
this.config.ports.apiPort = newPort;
}
}
};
isInPortRange = (number) => {
if (number < 1024 || number > 65535) {
this.ui.showErrorMessage("Port should be between 1024 and 65535.");
return false;
}
}
return true;
}
};
saveChangesAndExit = async () => {
this.configService.saveConfig();
this.ui.showInfoMessage("Configuration changes saved.");
this.running = false;
}
this.loop.stopLoop();
};
discardChangesAndExit = async () => {
this.configService.loadConfig();
this.ui.showInfoMessage("Changes discarded.");
this.running = false;
}
this.loop.stopLoop();
};
}

View File

@ -2,143 +2,242 @@ import { describe, beforeEach, it, expect, vi } from "vitest";
import { ConfigMenu } from "./configmenu.js";
import { mockUiService } from "../__mocks__/service.mocks.js";
import { mockConfigService } from "../__mocks__/service.mocks.js";
import { mockPathSelector, mockNumberSelector } from "../__mocks__/ui.mocks.js";
import {
mockPathSelector,
mockNumberSelector,
mockMenuLoop,
} from "../__mocks__/utils.mocks.js";
describe("ConfigMenu", () => {
const config = {
dataDir: "/data",
logsDir: "/logs",
storageQuota: 1024 * 1024 * 1024,
ports: {
discPort: 8090,
listenPort: 8070,
apiPort: 8080,
},
};
let configMenu;
beforeEach(() => {
vi.resetAllMocks();
mockConfigService.get.mockReturnValue({
dataDir: "/data",
logsDir: "/logs",
storageQuota: 1024 * 1024 * 1024,
ports: {
discPort: 8090,
listenPort: 8070,
apiPort: 8080,
}
});
mockConfigService.get.mockReturnValue(config);
configMenu = new ConfigMenu(
mockUiService,
mockMenuLoop,
mockConfigService,
mockPathSelector,
mockNumberSelector
mockNumberSelector,
);
});
// it("displays the configuration menu", async () => {
// configMenu.running = false; // Prevent infinite loop
// await configMenu.show();
it("initializes the loop with the config menu", () => {
expect(mockMenuLoop.initialize).toHaveBeenCalledWith(
configMenu.showConfigMenu,
);
});
// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
// "Codex Configuration",
// );
// expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith([
// {
// label: `Data path = "${mockConfigService.get().dataDir}"`,
// action: configMenu.editDataDir,
// },
// {
// label: `Logs path = "${mockConfigService.get().logsDir}"`,
// action: configMenu.editLogsDir,
// },
// {
// label: `Storage quota = 1Gb`,
// action: configMenu.editStorageQuota,
// },
// {
// label: `Discovery port = ${mockConfigService.get().ports.discPort}`,
// action: configMenu.editDiscPort,
// },
// {
// label: `P2P listen port = ${mockConfigService.get().ports.listenPort}`,
// action: configMenu.editListenPort,
// },
// {
// label: `API port = ${mockConfigService.get().ports.apiPort}`,
// action: configMenu.editApiPort,
// },
// {
// label: "Save changes and exit",
// action: configMenu.saveChangesAndExit,
// },
// {
// label: "Discard changes and exit",
// action: configMenu.discardChangesAndExit,
// }
// ]);
// });
it("shows the config menu header", async () => {
await configMenu.show();
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"Codex Configuration",
);
});
// it("edits the logs directory", async () => {
// mockPathSelector.show.mockResolvedValue("/new-logs");
// await configMenu.editLogsDir();
it("starts the menu loop", async () => {
await configMenu.show();
expect(mockMenuLoop.showLoop).toHaveBeenCalled();
});
// expect(mockPathSelector.show).toHaveBeenCalledWith("/logs", true);
// expect(configMenu.config.logsDir).toEqual("/new-logs");
// });
it("sets the config field", async () => {
await configMenu.show();
expect(configMenu.config).toEqual(config);
});
// it("edits the storage quota", async () => {
// mockNumberSelector.show.mockResolvedValue(200 * 1024 * 1024);
// await configMenu.editStorageQuota();
describe("config menu options", () => {
beforeEach(() => {
configMenu.config = config;
});
// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
// "You can use: 'GB' or 'gb', etc.",
// );
// expect(mockNumberSelector.show).toHaveBeenCalledWith(
// 1024 * 1024 * 1024,
// "Storage quota",
// true,
// );
// expect(configMenu.config.storageQuota).toEqual(200 * 1024 * 1024);
// });
it("displays the configuration menu", async () => {
await configMenu.showConfigMenu();
expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith(
"Select to edit:",
[
{
label: `Data path = "${mockConfigService.get().dataDir}"`,
action: configMenu.editDataDir,
},
{
label: `Logs path = "${mockConfigService.get().logsDir}"`,
action: configMenu.editLogsDir,
},
{
label: `Storage quota = 1073741824 Bytes (1024 MB)`,
action: configMenu.editStorageQuota,
},
{
label: `Discovery port = ${mockConfigService.get().ports.discPort}`,
action: configMenu.editDiscPort,
},
{
label: `P2P listen port = ${mockConfigService.get().ports.listenPort}`,
action: configMenu.editListenPort,
},
{
label: `API port = ${mockConfigService.get().ports.apiPort}`,
action: configMenu.editApiPort,
},
{
label: "Save changes and exit",
action: configMenu.saveChangesAndExit,
},
{
label: "Discard changes and exit",
action: configMenu.discardChangesAndExit,
},
],
);
});
// it("shows an error if storage quota is too small", async () => {
// mockNumberSelector.show.mockResolvedValue(50 * 1024 * 1024);
// await configMenu.editStorageQuota();
it("edits the logs directory", async () => {
const originalPath = config.logsDir;
mockPathSelector.show.mockResolvedValue("/new-logs");
await configMenu.editLogsDir();
// expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(
// "Storage quote should be >= 100mb.",
// );
// expect(configMenu.config.storageQuota).toEqual(1024 * 1024 * 1024); // Unchanged
// });
expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, true);
expect(configMenu.config.logsDir).toEqual("/new-logs");
});
// it("edits the discovery port", async () => {
// mockNumberSelector.show.mockResolvedValue(9000);
// await configMenu.editDiscPort();
it("edits the storage quota", async () => {
const originalQuota = config.storageQuota;
const newQuota = 200 * 1024 * 1024;
mockNumberSelector.show.mockResolvedValue(newQuota);
// expect(mockNumberSelector.show).toHaveBeenCalledWith(8090, "Discovery port", false);
// expect(configMenu.config.ports.discPort).toEqual(9000);
// });
await configMenu.editStorageQuota();
// it("shows an error if port is out of range", async () => {
// mockNumberSelector.show.mockResolvedValue(1000);
// await configMenu.editDiscPort();
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"You can use: 'GB' or 'gb', etc.",
);
expect(mockNumberSelector.show).toHaveBeenCalledWith(
originalQuota,
"Storage quota",
true,
);
expect(configMenu.config.storageQuota).toEqual(newQuota);
});
// expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(
// "Port should be between 1024 and 65535.",
// );
// expect(configMenu.config.ports.discPort).toEqual(8090); // Unchanged
// });
it("shows an error if storage quota is too small", async () => {
const originalQuota = config.storageQuota;
mockNumberSelector.show.mockResolvedValue(50 * 1024 * 1024);
// it("saves changes and exits", async () => {
// await configMenu.saveChangesAndExit();
await configMenu.editStorageQuota();
// expect(mockConfigService.saveConfig).toHaveBeenCalled();
// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
// "Configuration changes saved.",
// );
// expect(configMenu.running).toEqual(false);
// });
expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(
"Storage quote should be >= 100mb.",
);
expect(configMenu.config.storageQuota).toEqual(originalQuota);
});
// it("discards changes and exits", async () => {
// await configMenu.discardChangesAndExit();
it("edits the discovery port", async () => {
const originalPort = config.ports.discPort;
const newPort = 9000;
mockNumberSelector.show.mockResolvedValue(newPort);
// expect(mockConfigService.loadConfig).toHaveBeenCalled();
// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
// "Changes discarded.",
// );
// expect(configMenu.running).toEqual(false);
// });
await configMenu.editDiscPort();
expect(mockNumberSelector.show).toHaveBeenCalledWith(
originalPort,
"Discovery port",
false,
);
expect(configMenu.config.ports.discPort).toEqual(newPort);
});
it("shows an error if discovery port is out of range", async () => {
const originalPort = config.ports.discPort;
mockNumberSelector.show.mockResolvedValue(1000);
await configMenu.editDiscPort();
expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(
"Port should be between 1024 and 65535.",
);
expect(configMenu.config.ports.discPort).toEqual(originalPort);
});
it("edits the listen port", async () => {
const originalPort = config.ports.listenPort;
const newPort = 9000;
mockNumberSelector.show.mockResolvedValue(newPort);
await configMenu.editListenPort();
expect(mockNumberSelector.show).toHaveBeenCalledWith(
originalPort,
"P2P listen port",
false,
);
expect(configMenu.config.ports.listenPort).toEqual(newPort);
});
it("shows an error if listen port is out of range", async () => {
const originalPort = config.ports.listenPort;
mockNumberSelector.show.mockResolvedValue(1000);
await configMenu.editListenPort();
expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(
"Port should be between 1024 and 65535.",
);
expect(configMenu.config.ports.listenPort).toEqual(originalPort);
});
it("edits the API port", async () => {
const originalPort = config.ports.apiPort;
const newPort = 9000;
mockNumberSelector.show.mockResolvedValue(newPort);
await configMenu.editApiPort();
expect(mockNumberSelector.show).toHaveBeenCalledWith(
originalPort,
"API port",
false,
);
expect(configMenu.config.ports.apiPort).toEqual(newPort);
});
it("shows an error if API port is out of range", async () => {
const originalPort = config.ports.apiPort;
mockNumberSelector.show.mockResolvedValue(1000);
await configMenu.editApiPort();
expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(
"Port should be between 1024 and 65535.",
);
expect(configMenu.config.ports.apiPort).toEqual(originalPort);
});
it("saves changes and exits", async () => {
await configMenu.saveChangesAndExit();
expect(mockConfigService.saveConfig).toHaveBeenCalled();
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"Configuration changes saved.",
);
expect(mockMenuLoop.stopLoop).toHaveBeenCalled();
});
it("discards changes and exits", async () => {
await configMenu.discardChangesAndExit();
expect(mockConfigService.loadConfig).toHaveBeenCalled();
expect(mockUiService.showInfoMessage).toHaveBeenCalledWith(
"Changes discarded.",
);
expect(mockMenuLoop.stopLoop).toHaveBeenCalled();
});
});
});

View File

@ -10,11 +10,18 @@ describe("mainmenu", () => {
beforeEach(() => {
vi.resetAllMocks();
mainmenu = new MainMenu(mockUiService, mockMenuLoop, mockInstallMenu, mockConfigMenu);
mainmenu = new MainMenu(
mockUiService,
mockMenuLoop,
mockInstallMenu,
mockConfigMenu,
);
});
it("initializes the menu loop with the promptMainMenu function", () => {
expect(mockMenuLoop.initialize).toHaveBeenCalledWith(mainmenu.promptMainMenu);
expect(mockMenuLoop.initialize).toHaveBeenCalledWith(
mainmenu.promptMainMenu,
);
});
it("shows the logo", async () => {

View File

@ -1,20 +1,20 @@
export class MenuLoop {
initialize = (menuPrompt) => {
this.menuPrompt = menuPrompt;
}
initialize = (menuPrompt) => {
this.menuPrompt = menuPrompt;
};
showOnce = async () => {
await this.menuPrompt();
}
showOnce = async () => {
await this.menuPrompt();
};
showLoop = async () => {
this.running = true;
while (this.running) {
await this.menuPrompt();
}
showLoop = async () => {
this.running = true;
while (this.running) {
await this.menuPrompt();
}
};
stopLoop = () => {
this.running = false;
}
stopLoop = () => {
this.running = false;
};
}

View File

@ -2,41 +2,41 @@ import { describe, beforeEach, it, expect, vi } from "vitest";
import { MenuLoop } from "./menuLoop.js";
describe("MenuLoop", () => {
let menuLoop;
const mockPrompt = vi.fn();
let menuLoop;
const mockPrompt = vi.fn();
beforeEach(() => {
vi.resetAllMocks();
menuLoop = new MenuLoop();
menuLoop.initialize(mockPrompt);
beforeEach(() => {
vi.resetAllMocks();
menuLoop = new MenuLoop();
menuLoop.initialize(mockPrompt);
});
it("can show menu once", async () => {
await menuLoop.showOnce();
expect(mockPrompt).toHaveBeenCalledTimes(1);
});
it("can stop the menu loop", async () => {
mockPrompt.mockImplementation(() => {
menuLoop.stopLoop();
});
await menuLoop.showLoop();
expect(mockPrompt).toHaveBeenCalledTimes(1);
expect(menuLoop.running).toBe(false);
});
it("can run menu in a loop", async () => {
let calls = 0;
mockPrompt.mockImplementation(() => {
calls++;
if (calls >= 3) {
menuLoop.stopLoop();
}
});
it("can show menu once", async () => {
await menuLoop.showOnce();
expect(mockPrompt).toHaveBeenCalledTimes(1);
});
await menuLoop.showLoop();
it("can stop the menu loop", async () => {
mockPrompt.mockImplementation(() => {
menuLoop.stopLoop();
});
await menuLoop.showLoop();
expect(mockPrompt).toHaveBeenCalledTimes(1);
expect(menuLoop.running).toBe(false);
});
it("can run menu in a loop", async () => {
let calls = 0;
mockPrompt.mockImplementation(() => {
calls++;
if (calls >= 3) {
menuLoop.stopLoop();
}
});
await menuLoop.showLoop();
expect(mockPrompt).toHaveBeenCalledTimes(3);
});
expect(mockPrompt).toHaveBeenCalledTimes(3);
});
});

View File

@ -3,11 +3,7 @@ export class NumberSelector {
this.uiService = uiService;
}
show = async (
currentValue,
promptMessage,
allowMetricPostfixes,
) => {
show = async (currentValue, promptMessage, allowMetricPostfixes) => {
try {
var valueStr = await this.promptForValueStr(promptMessage);
valueStr = valueStr.replaceAll(" ", "");

View File

@ -34,11 +34,7 @@ describe("number selector", () => {
mockUiService.askPrompt.mockResolvedValue("what?!");
const number = await numberSelector.show(
currentValue,
prompt,
false,
);
const number = await numberSelector.show(currentValue, prompt, false);
expect(number).toEqual(currentValue);
});

View File

@ -106,14 +106,14 @@ export class PathSelector {
console.log("The path does not exist.");
}
this.updateCurrentIfValidParts(this.splitPath(newFullPath));
}
};
updateCurrentIfValidParts = (newParts) => {
if (!this.hasValidRoot(newParts)) {
console.log("The path has no valid root.");
}
this.currentPath = newParts;
}
};
enterPath = async () => {
const newPath = await this.ui.askPrompt("Enter Path:");
@ -156,8 +156,8 @@ export class PathSelector {
action: () => {
selected = option;
},
})
})
});
});
await this.ui.askMultipleChoice("Select an subdir", uiOptions);
@ -174,7 +174,7 @@ export class PathSelector {
selectThisPath = async () => {
this.resultingPath = this.combine(this.currentPath);
this.running = false;
}
};
cancel = async () => {
this.resultingPath = this.startingPath;