mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-17 14:57:49 +00:00
fix(settings): harden settings and diagnostic controls
This commit is contained in:
@@ -200,6 +200,9 @@ describe('useDownloadStore', () => {
|
||||
expect(normalizeCustomProxy(' socks5://127.0.0.1 ', 1080)).toBeNull();
|
||||
expect(normalizeCustomProxy('https://proxy.local', 8443)).toBeNull();
|
||||
expect(normalizeCustomProxy('127.0.0.1', NaN)).toBeNull();
|
||||
expect(normalizeCustomProxy('127.0.0.1:9000', 8080)).toBeNull();
|
||||
expect(normalizeCustomProxy('127.0.0.1/path', 8080)).toBeNull();
|
||||
expect(normalizeCustomProxy('[::1]', 8080)).toBe('http://[::1]:8080');
|
||||
|
||||
expect(await getProxyArgs({
|
||||
proxyMode: 'none',
|
||||
|
||||
@@ -276,6 +276,7 @@ export const normalizeCustomProxy = (host: string, port: number): string | null
|
||||
try {
|
||||
const parsed = new URL(trimmedHost);
|
||||
if (parsed.protocol !== 'http:') return null;
|
||||
if (!parsed.hostname) return null;
|
||||
if (!parsed.port) parsed.port = String(normalizedPort);
|
||||
return parsed.toString().replace(/\/$/, '');
|
||||
} catch {
|
||||
@@ -283,7 +284,24 @@ export const normalizeCustomProxy = (host: string, port: number): string | null
|
||||
}
|
||||
}
|
||||
|
||||
return `http://${trimmedHost}:${normalizedPort}`;
|
||||
try {
|
||||
const parsed = new URL(`http://${trimmedHost}:${normalizedPort}`);
|
||||
if (
|
||||
!parsed.hostname
|
||||
|| parsed.username
|
||||
|| parsed.password
|
||||
|| parsed.pathname !== '/'
|
||||
|| parsed.search
|
||||
|| parsed.hash
|
||||
|| (parsed.port && Number(parsed.port) !== normalizedPort)
|
||||
|| (!parsed.port && normalizedPort !== 80)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return `http://${trimmedHost}:${normalizedPort}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getProxyArgs = async (settings: ReturnType<typeof useSettingsStore.getState>) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { useSettingsStore } from './useSettingsStore';
|
||||
import { subscribeToSettingsPersistenceErrors, useSettingsStore } from './useSettingsStore';
|
||||
import * as ipc from '../ipc';
|
||||
|
||||
vi.mock('../ipc', () => ({
|
||||
@@ -66,3 +66,26 @@ describe('useSettingsStore credential-store startup flow', () => {
|
||||
expect(useSettingsStore.getState().keychainPromptDismissed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSettingsStore persistence failures', () => {
|
||||
it('reports a database save failure and retries the next settings update', async () => {
|
||||
vi.clearAllMocks();
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const onPersistenceError = vi.fn();
|
||||
const unsubscribe = subscribeToSettingsPersistenceErrors(onPersistenceError);
|
||||
vi.mocked(ipc.invokeCommand).mockRejectedValueOnce(new Error('database unavailable'));
|
||||
|
||||
useSettingsStore.setState({ theme: 'dark' });
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(onPersistenceError).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.mocked(ipc.invokeCommand).mockResolvedValueOnce(undefined);
|
||||
useSettingsStore.setState({ theme: 'light' });
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
expect(onPersistenceError).toHaveBeenCalledTimes(1);
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,9 +20,29 @@ import {
|
||||
import { normalizeSpeedLimitForBackend } from '../utils/downloads';
|
||||
|
||||
let settingsSave = Promise.resolve();
|
||||
const settingsPersistenceErrorListeners = new Set<() => void>();
|
||||
let settingsPersistenceFailed = false;
|
||||
const DEFAULT_SCHEDULER_QUEUE_ID = '00000000-0000-0000-0000-000000000001';
|
||||
export const DEFAULT_SPEED_LIMIT_PRESET_VALUES = [1, 5, 10];
|
||||
|
||||
export const subscribeToSettingsPersistenceErrors = (listener: () => void): (() => void) => {
|
||||
settingsPersistenceErrorListeners.add(listener);
|
||||
if (settingsPersistenceFailed) listener();
|
||||
return () => settingsPersistenceErrorListeners.delete(listener);
|
||||
};
|
||||
|
||||
const notifySettingsPersistenceError = () => {
|
||||
if (settingsPersistenceFailed) return;
|
||||
settingsPersistenceFailed = true;
|
||||
for (const listener of settingsPersistenceErrorListeners) {
|
||||
try {
|
||||
listener();
|
||||
} catch (error) {
|
||||
console.error('Settings persistence error listener failed', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const THEME_VALUES = ['system', 'light', 'dark', 'dracula', 'nord'] as const;
|
||||
const APP_FONT_SIZE_VALUES = ['small', 'standard', 'large'] as const;
|
||||
const LIST_ROW_DENSITY_VALUES = ['compact', 'standard', 'relaxed'] as const;
|
||||
@@ -83,9 +103,13 @@ const tauriStorage: StateStorage = {
|
||||
setItem: async (name: string, value: string): Promise<void> => {
|
||||
if (name === 'firelink-settings') {
|
||||
settingsSave = settingsSave
|
||||
.catch(() => undefined)
|
||||
.then(() => invoke('db_save_settings', { data: value }))
|
||||
.catch(e => {
|
||||
console.error("Failed to save settings to DB", e);
|
||||
.then(() => {
|
||||
settingsPersistenceFailed = false;
|
||||
}, () => {
|
||||
console.error('Failed to save settings to DB');
|
||||
notifySettingsPersistenceError();
|
||||
});
|
||||
await settingsSave;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user