release: v0.19.0
Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of [CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md) for the full list. Highlights: client proofing links (#683), custom content pages (#738), asset picker search (#740), concurrent-edit detection (#742), env-locked settings (#739), an accessibility baseline (#741), three security fixes with advisories (#737, #755, #759), and a pre-release quality pass (#780–#797). --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ralf <rohlenbo79@gmail.com> Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com> Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com> Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
// Mock env.ts so importing config.ts doesn't trigger Zod validation
|
||||
vi.mock('@/lib/env', () => ({
|
||||
@@ -17,47 +20,109 @@ vi.mock('@/lib/secret', () => ({
|
||||
resolveAuthSecret: () => 'test-auth-secret-32-chars-long-min',
|
||||
}));
|
||||
|
||||
// Replace the filesystem-backed YAML layer. The real loadYaml() already
|
||||
// revalidates by mtime, so "the file changed on disk" is modelled by the
|
||||
// mock returning different content.
|
||||
vi.mock('@/lib/config/parser', () => ({
|
||||
loadYaml: vi.fn(),
|
||||
clearYamlCache: vi.fn(),
|
||||
validateUuid: (id: string) => id,
|
||||
}));
|
||||
// The real YAML layer, spied on: how often it is asked is how often the
|
||||
// config is derived.
|
||||
vi.mock('@/lib/config/parser', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/config/parser')>();
|
||||
return { ...actual, loadYaml: vi.fn(actual.loadYaml) };
|
||||
});
|
||||
|
||||
import { getConfig, invalidateConfigCache } from '@/lib/config';
|
||||
import { loadYaml } from '@/lib/config/parser';
|
||||
|
||||
const ALBUM_ID = '11111111-1111-1111-1111-111111111111';
|
||||
|
||||
function mockYamlFiles(settings: Record<string, unknown>) {
|
||||
vi.mocked(loadYaml).mockImplementation((filename: string) => {
|
||||
if (filename === 'gallery.yaml') return { albums: [ALBUM_ID] };
|
||||
if (filename === 'settings.yaml') return settings;
|
||||
return null;
|
||||
});
|
||||
let dir: string;
|
||||
let mtime = Date.now() / 1000;
|
||||
|
||||
/** Write a content file with a distinct mtime, the way a save from another worker lands. */
|
||||
function write(name: string, body: string) {
|
||||
const file = path.join(dir, 'content', name);
|
||||
fs.writeFileSync(file, body);
|
||||
mtime += 10;
|
||||
fs.utimesSync(file, mtime, mtime);
|
||||
}
|
||||
|
||||
describe('getConfig() in production', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('NODE_ENV', 'production');
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'folio-config-cache-'));
|
||||
fs.mkdirSync(path.join(dir, 'content'));
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(dir);
|
||||
write('gallery.yaml', `albums:\n - "${ALBUM_ID}"\n`);
|
||||
write('settings.yaml', 'title: Old Title\n');
|
||||
invalidateConfigCache();
|
||||
vi.mocked(loadYaml).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('picks up settings.yaml changes without invalidateConfigCache()', () => {
|
||||
// An admin save may run in a different worker/process than page
|
||||
// rendering, so this worker never sees invalidateConfigCache().
|
||||
// getConfig() must delegate freshness to the mtime-checked YAML layer
|
||||
// instead of short-circuiting on a stale in-memory copy.
|
||||
mockYamlFiles({ title: 'Old Title' });
|
||||
// getConfig() must notice the file changed instead of short-circuiting
|
||||
// on a stale in-memory copy.
|
||||
expect(getConfig().siteTitle).toBe('Old Title');
|
||||
|
||||
mockYamlFiles({ title: 'New Title' });
|
||||
write('settings.yaml', 'title: New Title\n');
|
||||
expect(getConfig().siteTitle).toBe('New Title');
|
||||
});
|
||||
|
||||
it('picks up gallery.yaml changes without invalidateConfigCache()', () => {
|
||||
expect(getConfig().albums).toEqual([ALBUM_ID]);
|
||||
|
||||
const other = '22222222-2222-2222-2222-222222222222';
|
||||
write('gallery.yaml', `albums:\n - "${ALBUM_ID}"\n - "${other}"\n`);
|
||||
expect(getConfig().albums).toEqual([ALBUM_ID, other]);
|
||||
});
|
||||
|
||||
it('notices about.md appearing', () => {
|
||||
expect(getConfig().aboutEnabled).toBe(false);
|
||||
|
||||
write('about.md', '---\nname: Someone\n---\n');
|
||||
expect(getConfig().aboutEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('notices install.json credentials appearing', () => {
|
||||
// env supplies the URL; the file is still watched.
|
||||
write('install.json', JSON.stringify({ apiUrl: 'http://file:2283', apiKey: 'file-key' }));
|
||||
getConfig();
|
||||
expect(vi.mocked(loadYaml)).toHaveBeenCalledTimes(2);
|
||||
|
||||
write('install.json', JSON.stringify({ apiUrl: 'http://file:2283', apiKey: 'other' }));
|
||||
getConfig();
|
||||
expect(vi.mocked(loadYaml)).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
// Every image request and every page render call getConfig() many times
|
||||
// over. Deriving the whole config each time cost ~150µs a call; the files
|
||||
// did not change in between.
|
||||
it('derives the config once while its files are unchanged', () => {
|
||||
for (let i = 0; i < 100; i++) getConfig();
|
||||
|
||||
// gallery.yaml and settings.yaml, once — not 200 reads.
|
||||
expect(vi.mocked(loadYaml)).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('derives again after invalidateConfigCache()', () => {
|
||||
getConfig();
|
||||
invalidateConfigCache();
|
||||
getConfig();
|
||||
|
||||
expect(vi.mocked(loadYaml)).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('hands every caller its own copy', () => {
|
||||
const first = getConfig();
|
||||
first.albums.push('mutated');
|
||||
first.siteTitle = 'mutated';
|
||||
|
||||
const second = getConfig();
|
||||
expect(second.albums).toEqual([ALBUM_ID]);
|
||||
expect(second.siteTitle).toBe('Old Title');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user