mirror of
https://github.com/Pandipipas/scoreko-electron-dev.git
synced 2026-06-06 05:32:06 +00:00
feat: Implement update management refactor with new dialog and settings handling
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
# Phase 3 Summary
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Executed the UI and settings cleanup phase only for the Electron package.
|
||||||
|
|
||||||
|
Documentation used as source of truth:
|
||||||
|
|
||||||
|
- `docs/refactor/ARCHITECTURE_AUDIT.md`
|
||||||
|
- `docs/refactor/ARCHITECTURE_RULES.md`
|
||||||
|
- `docs/refactor/TARGET_ARCHITECTURE.md`
|
||||||
|
- `docs/refactor/MIGRATION_PLAN.md`
|
||||||
|
- `docs/refactor/SESSION_HANDOFF.md`
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
- Split update dialog UI out of `src/main/updates/update-manager.ts` into `src/main/updates/update-dialogs.ts`.
|
||||||
|
- Split update settings loading and file-config normalization into `src/main/updates/update-settings.ts`.
|
||||||
|
- Split installer download behavior into `src/main/updates/update-download.ts`.
|
||||||
|
- Kept `src/main/updates/update-manager.ts` focused on orchestration:
|
||||||
|
- load settings
|
||||||
|
- fetch latest release
|
||||||
|
- ask the user what to do
|
||||||
|
- download installer
|
||||||
|
- run install handoff
|
||||||
|
- Added defensive update config parsing from `unknown` JSON without introducing `any`.
|
||||||
|
- Added settings tests covering:
|
||||||
|
- runtime config disabling updates
|
||||||
|
- runtime config overriding file settings
|
||||||
|
- malformed update config normalization
|
||||||
|
- invalid JSON fallback and logging
|
||||||
|
- Fixed the existing Spanish mojibake in update dialogs touched by this phase.
|
||||||
|
|
||||||
|
## Intentionally Not Changed
|
||||||
|
|
||||||
|
- No UX flow changes.
|
||||||
|
- No new features.
|
||||||
|
- No custom renderer was added.
|
||||||
|
- No preload was added.
|
||||||
|
- No IPC was added.
|
||||||
|
- No parent bundle source was modified.
|
||||||
|
- No generated `dist` or `lib` source was edited manually.
|
||||||
|
- No forms or controls were changed in the NodeCG dashboard.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Commands run successfully:
|
||||||
|
|
||||||
|
```text
|
||||||
|
npm run typecheck
|
||||||
|
npm test
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
Current test result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
59 tests passing
|
||||||
|
```
|
||||||
|
|
||||||
|
Sanity searches:
|
||||||
|
|
||||||
|
```text
|
||||||
|
rg -n "\bany\b" src/main src/tests
|
||||||
|
rg -n "ActualizaciÃ|estÃ|versiÃ|cerrarÃ" src/main src/tests
|
||||||
|
rg -n "ipcMain|ipcRenderer|contextBridge|preload|nodeIntegration:\s*true|webSecurity:\s*false" src/main src/tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Result:
|
||||||
|
|
||||||
|
- No `any` was introduced.
|
||||||
|
- No touched Spanish update-dialog text remains mojibaked.
|
||||||
|
- No production IPC or preload surface exists.
|
||||||
|
- No unsafe Electron window settings were introduced.
|
||||||
|
- Remaining IPC/preload matches are limited to the regression test that guards the zero-surface policy.
|
||||||
|
|
||||||
|
## UI Verification
|
||||||
|
|
||||||
|
The Electron launch path prepared a temporary managed runtime, but the NodeCG child did not expose port `9090` within the verification window. To verify the served UI without touching the user's real runtime data, NodeCG was launched from a temporary Electron `userData` directory:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SCOREKO_APP_USER_DATA_DIRECTORY=scoreko-codex-ui-check
|
||||||
|
SCOREKO_UPDATES_ENABLED=false
|
||||||
|
ELECTRON_LOAD_DELAY_MS=0
|
||||||
|
```
|
||||||
|
|
||||||
|
The temporary NodeCG runtime served:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:9090 -> 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Browser verification loaded:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:9090/bundles/scoreko-dev/dashboard/scoreko-dev/main.html?standalone=true#/
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed UI signals:
|
||||||
|
|
||||||
|
- Page title: `Dashboard`
|
||||||
|
- Scoreko sidebar rendered.
|
||||||
|
- Main navigation rendered.
|
||||||
|
- `Settings` navigation entry rendered.
|
||||||
|
- Dashboard form controls rendered.
|
||||||
|
|
||||||
|
The temporary NodeCG process was stopped after verification.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { BrowserWindow, dialog } from "electron";
|
||||||
|
import type { MessageBoxOptions } from "electron";
|
||||||
|
|
||||||
|
import { ReleaseUpdate } from "./update-utils";
|
||||||
|
|
||||||
|
export type DownloadUpdateChoice = "download" | "open-release" | "dismiss";
|
||||||
|
|
||||||
|
export async function askToDownloadUpdate(
|
||||||
|
update: ReleaseUpdate,
|
||||||
|
releasePageUrl: string | undefined,
|
||||||
|
parentWindow: BrowserWindow | null,
|
||||||
|
): Promise<DownloadUpdateChoice> {
|
||||||
|
const result = await showMessageBox(parentWindow, {
|
||||||
|
type: "info",
|
||||||
|
title: "Actualización disponible",
|
||||||
|
message: `Scoreko ${update.version} está disponible.`,
|
||||||
|
detail: "Puedes descargarla ahora o seguir usando esta versión.",
|
||||||
|
buttons: releasePageUrl ? ["Descargar", "Ver release", "Ahora no"] : ["Descargar", "Ahora no"],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: releasePageUrl ? 2 : 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (releasePageUrl && result.response === 1) {
|
||||||
|
return "open-release";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.response === 0 ? "download" : "dismiss";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function askToInstallUpdate(update: ReleaseUpdate, parentWindow: BrowserWindow | null): Promise<boolean> {
|
||||||
|
const result = await showMessageBox(parentWindow, {
|
||||||
|
type: "question",
|
||||||
|
title: "Actualización descargada",
|
||||||
|
message: `Scoreko ${update.version} se ha descargado.`,
|
||||||
|
detail: "Para instalarla se cerrará Scoreko y se abrirá el instalador.",
|
||||||
|
buttons: ["Instalar y cerrar", "Luego"],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.response === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessageBox(parentWindow: BrowserWindow | null, options: MessageBoxOptions) {
|
||||||
|
return parentWindow ? dialog.showMessageBox(parentWindow, options) : dialog.showMessageBox(options);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { Readable } from "node:stream";
|
||||||
|
|
||||||
|
import { ReleaseUpdate, sanitizeFileName } from "./update-utils";
|
||||||
|
|
||||||
|
type UpdateDownloadConfig = {
|
||||||
|
tempDirectory: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function downloadInstaller(update: ReleaseUpdate, config: UpdateDownloadConfig): Promise<string> {
|
||||||
|
const safeFileName = sanitizeFileName(update.installer.name);
|
||||||
|
const downloadDirectory = path.join(config.tempDirectory, "scoreko-updates");
|
||||||
|
const targetPath = path.join(downloadDirectory, safeFileName);
|
||||||
|
|
||||||
|
fs.mkdirSync(downloadDirectory, { recursive: true });
|
||||||
|
|
||||||
|
const response = await fetch(update.installer.downloadUrl);
|
||||||
|
if (!response.ok || !response.body) {
|
||||||
|
throw new Error(`Could not download update installer. HTTP ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const fileStream = fs.createWriteStream(targetPath);
|
||||||
|
const responseStream = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);
|
||||||
|
|
||||||
|
responseStream.on("error", reject);
|
||||||
|
fileStream.on("error", reject);
|
||||||
|
fileStream.on("finish", resolve);
|
||||||
|
responseStream.pipe(fileStream);
|
||||||
|
});
|
||||||
|
|
||||||
|
return targetPath;
|
||||||
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import { app, BrowserWindow, dialog, shell } from "electron";
|
import { app, BrowserWindow, shell } from "electron";
|
||||||
import type { MessageBoxOptions } from "electron";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { Readable } from "node:stream";
|
|
||||||
|
|
||||||
import { AppRuntimeConfig } from "../config/runtime-config";
|
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||||
import { buildReleaseUpdate, GiteaRelease, ReleaseUpdate, sanitizeFileName, UpdateFileConfig } from "./update-utils";
|
import { askToDownloadUpdate, askToInstallUpdate } from "./update-dialogs";
|
||||||
|
import { downloadInstaller } from "./update-download";
|
||||||
|
import { loadUpdateSettings, UpdateSettings } from "./update-settings";
|
||||||
|
import { buildReleaseUpdate, GiteaRelease } from "./update-utils";
|
||||||
|
|
||||||
type UpdateManagerConfig = {
|
type UpdateManagerConfig = {
|
||||||
appConfig: AppRuntimeConfig;
|
appConfig: AppRuntimeConfig;
|
||||||
@@ -15,13 +14,6 @@ type UpdateManagerConfig = {
|
|||||||
log: (...args: unknown[]) => void;
|
log: (...args: unknown[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpdateSettings = {
|
|
||||||
enabled: boolean;
|
|
||||||
apiUrl?: string;
|
|
||||||
releasePageUrl?: string;
|
|
||||||
assetPattern: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function scheduleUpdateCheck({
|
export function scheduleUpdateCheck({
|
||||||
appConfig,
|
appConfig,
|
||||||
rootPath,
|
rootPath,
|
||||||
@@ -65,16 +57,19 @@ async function checkForUpdates({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldDownload = await askToDownloadUpdate(
|
const releasePageUrl = settings.releasePageUrl ?? update.pageUrl;
|
||||||
update,
|
const downloadChoice = await askToDownloadUpdate(update, releasePageUrl, getParentWindow());
|
||||||
settings.releasePageUrl ?? update.pageUrl,
|
|
||||||
getParentWindow(),
|
if (downloadChoice === "open-release") {
|
||||||
);
|
await openReleasePage(releasePageUrl);
|
||||||
if (!shouldDownload) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const installerPath = await downloadInstaller(update);
|
if (downloadChoice !== "download") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installerPath = await downloadInstaller(update, { tempDirectory: app.getPath("temp") });
|
||||||
const shouldInstall = await askToInstallUpdate(update, getParentWindow());
|
const shouldInstall = await askToInstallUpdate(update, getParentWindow());
|
||||||
if (!shouldInstall) {
|
if (!shouldInstall) {
|
||||||
await shell.showItemInFolder(installerPath);
|
await shell.showItemInFolder(installerPath);
|
||||||
@@ -93,41 +88,6 @@ async function checkForUpdates({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUpdateSettings(
|
|
||||||
appConfig: AppRuntimeConfig,
|
|
||||||
rootPath: string,
|
|
||||||
log: (...args: unknown[]) => void,
|
|
||||||
): UpdateSettings {
|
|
||||||
const fileConfig = readUpdateFileConfig(appConfig, rootPath, log);
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: appConfig.updatesEnabled && (Boolean(fileConfig.enabled) || Boolean(appConfig.updateApiUrl)),
|
|
||||||
apiUrl: appConfig.updateApiUrl ?? readOptionalString(fileConfig.apiUrl),
|
|
||||||
releasePageUrl: appConfig.updateReleasePageUrl ?? readOptionalString(fileConfig.releasePageUrl),
|
|
||||||
assetPattern:
|
|
||||||
appConfig.updateAssetPattern || readOptionalString(fileConfig.assetPattern) || "Scoreko-setup-.*\\.exe$",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readUpdateFileConfig(
|
|
||||||
appConfig: AppRuntimeConfig,
|
|
||||||
rootPath: string,
|
|
||||||
log: (...args: unknown[]) => void,
|
|
||||||
): UpdateFileConfig {
|
|
||||||
const configPath = appConfig.updateConfigPathOverride ?? path.join(rootPath, "static", "updates.json");
|
|
||||||
|
|
||||||
if (!fs.existsSync(configPath)) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return JSON.parse(fs.readFileSync(configPath, "utf8")) as UpdateFileConfig;
|
|
||||||
} catch (error) {
|
|
||||||
log(`Could not read update config at ${configPath}.`, error);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLatestRelease(apiUrl: string): Promise<GiteaRelease> {
|
async function fetchLatestRelease(apiUrl: string): Promise<GiteaRelease> {
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -142,72 +102,8 @@ async function fetchLatestRelease(apiUrl: string): Promise<GiteaRelease> {
|
|||||||
return (await response.json()) as GiteaRelease;
|
return (await response.json()) as GiteaRelease;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askToDownloadUpdate(
|
async function openReleasePage(releasePageUrl: string | undefined): Promise<void> {
|
||||||
update: ReleaseUpdate,
|
if (releasePageUrl) {
|
||||||
releasePageUrl: string | undefined,
|
|
||||||
parentWindow: BrowserWindow | null,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const result = await showMessageBox(parentWindow, {
|
|
||||||
type: "info",
|
|
||||||
title: "Actualización disponible",
|
|
||||||
message: `Scoreko ${update.version} está disponible.`,
|
|
||||||
detail: "Puedes descargarla ahora o seguir usando esta versión.",
|
|
||||||
buttons: releasePageUrl ? ["Descargar", "Ver release", "Ahora no"] : ["Descargar", "Ahora no"],
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: releasePageUrl ? 2 : 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (releasePageUrl && result.response === 1) {
|
|
||||||
await shell.openExternal(releasePageUrl);
|
await shell.openExternal(releasePageUrl);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.response === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function askToInstallUpdate(update: ReleaseUpdate, parentWindow: BrowserWindow | null): Promise<boolean> {
|
|
||||||
const result = await showMessageBox(parentWindow, {
|
|
||||||
type: "question",
|
|
||||||
title: "Actualización descargada",
|
|
||||||
message: `Scoreko ${update.version} se ha descargado.`,
|
|
||||||
detail: "Para instalarla se cerrará Scoreko y se abrirá el instalador.",
|
|
||||||
buttons: ["Instalar y cerrar", "Luego"],
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.response === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function downloadInstaller(update: ReleaseUpdate): Promise<string> {
|
|
||||||
const safeFileName = sanitizeFileName(update.installer.name);
|
|
||||||
const downloadDirectory = path.join(app.getPath("temp"), "scoreko-updates");
|
|
||||||
const targetPath = path.join(downloadDirectory, safeFileName);
|
|
||||||
|
|
||||||
fs.mkdirSync(downloadDirectory, { recursive: true });
|
|
||||||
|
|
||||||
const response = await fetch(update.installer.downloadUrl);
|
|
||||||
if (!response.ok || !response.body) {
|
|
||||||
throw new Error(`Could not download update installer. HTTP ${response.status}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const fileStream = fs.createWriteStream(targetPath);
|
|
||||||
const responseStream = Readable.fromWeb(response.body as Parameters<typeof Readable.fromWeb>[0]);
|
|
||||||
|
|
||||||
responseStream.on("error", reject);
|
|
||||||
fileStream.on("error", reject);
|
|
||||||
fileStream.on("finish", resolve);
|
|
||||||
responseStream.pipe(fileStream);
|
|
||||||
});
|
|
||||||
|
|
||||||
return targetPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readOptionalString(value: unknown): string | undefined {
|
|
||||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMessageBox(parentWindow: BrowserWindow | null, options: MessageBoxOptions) {
|
|
||||||
return parentWindow ? dialog.showMessageBox(parentWindow, options) : dialog.showMessageBox(options);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { AppRuntimeConfig } from "../config/runtime-config";
|
||||||
|
import { UpdateFileConfig } from "./update-utils";
|
||||||
|
|
||||||
|
const DEFAULT_UPDATE_ASSET_PATTERN = "Scoreko-setup-.*\\.exe$";
|
||||||
|
|
||||||
|
export type UpdateSettings = {
|
||||||
|
enabled: boolean;
|
||||||
|
apiUrl?: string;
|
||||||
|
releasePageUrl?: string;
|
||||||
|
assetPattern: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function loadUpdateSettings(
|
||||||
|
appConfig: AppRuntimeConfig,
|
||||||
|
rootPath: string,
|
||||||
|
log: (...args: unknown[]) => void,
|
||||||
|
): UpdateSettings {
|
||||||
|
const fileConfig = readUpdateFileConfig(appConfig, rootPath, log);
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: appConfig.updatesEnabled && (Boolean(fileConfig.enabled) || Boolean(appConfig.updateApiUrl)),
|
||||||
|
apiUrl: appConfig.updateApiUrl ?? readOptionalString(fileConfig.apiUrl),
|
||||||
|
releasePageUrl: appConfig.updateReleasePageUrl ?? readOptionalString(fileConfig.releasePageUrl),
|
||||||
|
assetPattern:
|
||||||
|
appConfig.updateAssetPattern || readOptionalString(fileConfig.assetPattern) || DEFAULT_UPDATE_ASSET_PATTERN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readUpdateFileConfig(
|
||||||
|
appConfig: AppRuntimeConfig,
|
||||||
|
rootPath: string,
|
||||||
|
log: (...args: unknown[]) => void,
|
||||||
|
): UpdateFileConfig {
|
||||||
|
const configPath = appConfig.updateConfigPathOverride ?? path.join(rootPath, "static", "updates.json");
|
||||||
|
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedConfig: unknown = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||||
|
return normalizeUpdateFileConfig(parsedConfig);
|
||||||
|
} catch (error) {
|
||||||
|
log(`Could not read update config at ${configPath}.`, error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUpdateFileConfig(value: unknown): UpdateFileConfig {
|
||||||
|
if (!isRecord(value)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: value.enabled,
|
||||||
|
apiUrl: value.apiUrl,
|
||||||
|
releasePageUrl: value.releasePageUrl,
|
||||||
|
assetPattern: value.assetPattern,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function readOptionalString(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { AppRuntimeConfig } from "../main/config/runtime-config";
|
||||||
|
import { loadUpdateSettings, readUpdateFileConfig } from "../main/updates/update-settings";
|
||||||
|
|
||||||
|
const baseConfig: AppRuntimeConfig = {
|
||||||
|
title: "Scoreko",
|
||||||
|
userModelId: "com.scoreko.desktop",
|
||||||
|
userDataDirectoryName: "scoreko",
|
||||||
|
nodecgPort: "9090",
|
||||||
|
bundleName: "scoreko-dev",
|
||||||
|
mainDashboardRoute: "dashboard/scoreko-dev/main.html?standalone=true",
|
||||||
|
loadingDashboardRoute: "dashboard/loading/main.html?standalone=true",
|
||||||
|
loadDelayMs: 0,
|
||||||
|
startupTimeoutMs: 30000,
|
||||||
|
nodecgKillTimeoutMs: 2500,
|
||||||
|
updatesEnabled: true,
|
||||||
|
updateCheckDelayMs: 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
test("loadUpdateSettings keeps updates disabled when the runtime config disables them", () => {
|
||||||
|
const rootPath = makeTempRoot({
|
||||||
|
enabled: true,
|
||||||
|
apiUrl: "https://gitea.local/releases/latest",
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = loadUpdateSettings({ ...baseConfig, updatesEnabled: false }, rootPath, () => undefined);
|
||||||
|
|
||||||
|
assert.equal(settings.enabled, false);
|
||||||
|
assert.equal(settings.apiUrl, "https://gitea.local/releases/latest");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loadUpdateSettings lets runtime config override file settings", () => {
|
||||||
|
const rootPath = makeTempRoot({
|
||||||
|
enabled: true,
|
||||||
|
apiUrl: "https://file.local/releases/latest",
|
||||||
|
releasePageUrl: "https://file.local/releases",
|
||||||
|
assetPattern: "File-.*\\.exe$",
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = loadUpdateSettings(
|
||||||
|
{
|
||||||
|
...baseConfig,
|
||||||
|
updateApiUrl: "https://env.local/releases/latest",
|
||||||
|
updateReleasePageUrl: "https://env.local/releases",
|
||||||
|
updateAssetPattern: "Env-.*\\.exe$",
|
||||||
|
},
|
||||||
|
rootPath,
|
||||||
|
() => undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(settings, {
|
||||||
|
enabled: true,
|
||||||
|
apiUrl: "https://env.local/releases/latest",
|
||||||
|
releasePageUrl: "https://env.local/releases",
|
||||||
|
assetPattern: "Env-.*\\.exe$",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("readUpdateFileConfig normalizes malformed config into an empty file config", () => {
|
||||||
|
const rootPath = makeTempRoot(["not", "an", "object"]);
|
||||||
|
|
||||||
|
assert.deepEqual(readUpdateFileConfig(baseConfig, rootPath, () => undefined), {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("readUpdateFileConfig logs invalid JSON and returns an empty file config", () => {
|
||||||
|
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "scoreko-update-settings-"));
|
||||||
|
const staticPath = path.join(rootPath, "static");
|
||||||
|
fs.mkdirSync(staticPath, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(staticPath, "updates.json"), "{ invalid", "utf8");
|
||||||
|
const messages: unknown[][] = [];
|
||||||
|
|
||||||
|
const settings = readUpdateFileConfig(baseConfig, rootPath, (...args: unknown[]) => {
|
||||||
|
messages.push(args);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(settings, {});
|
||||||
|
assert.equal(messages.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeTempRoot(config: unknown): string {
|
||||||
|
const rootPath = fs.mkdtempSync(path.join(os.tmpdir(), "scoreko-update-settings-"));
|
||||||
|
const staticPath = path.join(rootPath, "static");
|
||||||
|
|
||||||
|
fs.mkdirSync(staticPath, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(staticPath, "updates.json"), JSON.stringify(config), "utf8");
|
||||||
|
|
||||||
|
return rootPath;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user