mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(config-backup): probe a custom folder before the retention fallback reads it, test that Save omits a displayed-only retention
This commit is contained in:
1 parent
9c0fc30927
commit
8a4ff775fb
9 files changed
+173
-25
No files matched your search
@@ -106,3 +106,49 @@ describe('loadConfigBackups (retention fields)', () => {
|
||||
expect(inputs.appSettingsConfigBackupKeepCount.value).toBe('20'); // the open-settings code owns it
|
||||
});
|
||||
});
|
||||
|
||||
describe('Save and the displayed retention', () => {
|
||||
function payload(
|
||||
prev: Record<string, unknown>,
|
||||
touched: { count?: boolean; days?: boolean },
|
||||
count = '100',
|
||||
days = '0'
|
||||
) {
|
||||
const inputs: Record<string, { value: string; dataset: Record<string, string> }> = {
|
||||
appSettingsConfigBackupKeepCount: { value: count, dataset: touched.count ? { touched: '1' } : {} },
|
||||
appSettingsConfigBackupKeepDays: { value: days, dataset: touched.days ? { touched: '1' } : {} },
|
||||
};
|
||||
function CodemanApp(this: unknown) {}
|
||||
const ctx = vm.createContext({
|
||||
CodemanApp,
|
||||
window: {},
|
||||
document: { getElementById: (id: string) => inputs[id] },
|
||||
});
|
||||
vm.runInContext(SRC, ctx);
|
||||
const app = Object.create((CodemanApp as unknown as { prototype: object }).prototype) as Record<string, unknown>;
|
||||
return (app._configBackupRetentionPayload as (p: Record<string, unknown>) => Record<string, unknown>).call(
|
||||
app,
|
||||
prev
|
||||
);
|
||||
}
|
||||
|
||||
it('leaves out a retention value that was only displayed (a reset settings.json)', () => {
|
||||
expect(payload({}, {})).toEqual({});
|
||||
});
|
||||
|
||||
it('sends a value that was saved before, or edited here, and only that one', () => {
|
||||
expect(payload({ configBackupKeepCount: 100 }, {})).toEqual({ configBackupKeepCount: 100 });
|
||||
expect(payload({}, { days: true })).toEqual({ configBackupKeepDays: 0 });
|
||||
expect(payload({}, { count: true, days: true }, '5', '7')).toEqual({
|
||||
configBackupKeepCount: 5,
|
||||
configBackupKeepDays: 7,
|
||||
});
|
||||
});
|
||||
|
||||
it('clamps like the form always did', () => {
|
||||
expect(payload({}, { count: true, days: true }, '1', '99999')).toEqual({
|
||||
configBackupKeepCount: 2,
|
||||
configBackupKeepDays: 3650,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @fileoverview A custom backup folder that stops answering (a network mount) must never be listed or
|
||||
* read before the bounded probe says it is there: GET, diff and restore answer 503 without touching it.
|
||||
* With only `configBackupDir` in settings.json the recorded-retention fallback would otherwise
|
||||
* `readdirSync` the folder first (found in review of #661).
|
||||
*
|
||||
* ⚠️ test/setup.ts gives the whole FILE one temp HOME. Port: N/A (inject()).
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const dead = vi.hoisted(() => ({ dir: '', touched: [] as string[] }));
|
||||
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:fs')>();
|
||||
const record = <F extends (...a: never[]) => unknown>(name: string, real: F): F =>
|
||||
((path: unknown, ...rest: unknown[]) => {
|
||||
if (dead.dir && String(path).startsWith(dead.dir)) dead.touched.push(`${name} ${String(path)}`);
|
||||
return (real as unknown as (...a: unknown[]) => unknown)(path, ...rest);
|
||||
}) as unknown as F;
|
||||
const wrapped = {
|
||||
...actual,
|
||||
readdirSync: record('readdirSync', actual.readdirSync),
|
||||
readFileSync: record('readFileSync', actual.readFileSync),
|
||||
statSync: record('statSync', actual.statSync),
|
||||
lstatSync: record('lstatSync', actual.lstatSync),
|
||||
existsSync: record('existsSync', actual.existsSync),
|
||||
};
|
||||
return { ...wrapped, default: wrapped };
|
||||
});
|
||||
|
||||
vi.mock('../../src/utils/bounded-path-probe.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/utils/bounded-path-probe.js')>();
|
||||
return {
|
||||
...actual,
|
||||
probePathKind: async (path: string, options?: { pastCap?: boolean }) =>
|
||||
dead.dir && path.startsWith(dead.dir) ? 'unknown' : actual.probePathKind(path, options),
|
||||
};
|
||||
});
|
||||
|
||||
import { createRouteTestHarness } from './_route-test-utils.js';
|
||||
import { registerConfigBackupRoutes } from '../../src/web/routes/config-backup-routes.js';
|
||||
import { SETTINGS_PATH } from '../../src/web/route-helpers.js';
|
||||
import { getDataDir } from '../../src/config/instance.js';
|
||||
import { configBackupOrigin } from '../../src/config-backup.js';
|
||||
|
||||
let root: string;
|
||||
beforeEach(() => {
|
||||
root = mkdtempSync(join(tmpdir(), 'cfg-backup-dead-'));
|
||||
dead.dir = join(root, 'mount');
|
||||
dead.touched.length = 0;
|
||||
configBackupOrigin(getDataDir()); // an instance that already has an id: the fallback has something to look for
|
||||
mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
|
||||
// Only the folder: no retention keys, the shape that makes the recorded-retention fallback want to read it.
|
||||
writeFileSync(SETTINGS_PATH, JSON.stringify({ configBackupDir: dead.dir }));
|
||||
});
|
||||
afterEach(() => {
|
||||
dead.dir = '';
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
rmSync(SETTINGS_PATH, { force: true });
|
||||
});
|
||||
|
||||
describe('a custom backup folder that does not answer', () => {
|
||||
it.each([
|
||||
['GET', '/api/config-backups'],
|
||||
['GET', '/api/config-backups/diff?from=cfg-20261001-120000'],
|
||||
['POST', '/api/config-backups/cfg-20261001-120000/restore'],
|
||||
] as const)('%s %s answers 503 without reading the folder', async (method, url) => {
|
||||
const { app } = await createRouteTestHarness(registerConfigBackupRoutes);
|
||||
const res = await app.inject({ method, url });
|
||||
expect(res.statusCode).toBe(503);
|
||||
expect(dead.touched).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user