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>
109 lines
4.4 KiB
TypeScript
109 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'fs/promises';
|
|
|
|
/**
|
|
* A page rename or delete moves the file first and then follows up the menu
|
|
* reference in gallery.yaml (#722). When that follow-up fails — gallery.yaml
|
|
* edited by hand into invalid YAML, a write error — the page has already been
|
|
* renamed or deleted. Answering 500 then told the admin nothing was saved: the
|
|
* panel kept the old slug, and every retry hit a 404 for a file that was gone.
|
|
*
|
|
* Real services on a temp content/ directory, as in concurrent-edits.test.ts.
|
|
*/
|
|
|
|
vi.mock('@/lib/admin/auth', () => ({
|
|
isAdminEnabled: vi.fn(() => true),
|
|
isAdminAuthenticated: vi.fn(async () => true),
|
|
COOKIE_NAME: 'folio_admin_session',
|
|
}));
|
|
vi.mock('@/lib/config', () => ({
|
|
invalidateConfigCache: vi.fn(),
|
|
isPageRef: (e: unknown) => !!e && typeof e === 'object' && 'page' in e,
|
|
}));
|
|
vi.mock('@/lib/admin/pageSlugs', () => ({
|
|
takenPageSlugs: vi.fn(async () => ({ subpages: [], albums: [], journal: [] })),
|
|
}));
|
|
vi.mock('next/cache', () => ({ revalidatePath: vi.fn() }));
|
|
|
|
let root: string;
|
|
let content: string;
|
|
|
|
beforeEach(async () => {
|
|
root = await mkdtemp(path.join(os.tmpdir(), 'folio-page-refs-'));
|
|
content = path.join(root, 'content');
|
|
await mkdir(path.join(content, 'pages'), { recursive: true });
|
|
vi.spyOn(process, 'cwd').mockReturnValue(root);
|
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
vi.resetModules();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
vi.restoreAllMocks();
|
|
await rm(root, { recursive: true, force: true });
|
|
});
|
|
|
|
const req = (method: string, body?: unknown) =>
|
|
new Request('http://localhost/api/admin/pages/x', {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
|
|
const md = (title: string) => `---\ntitle: "${title}"\n---\n\nBody of ${title}\n`;
|
|
const slugCtx = (slug: string) => ({ params: Promise.resolve({ slug }) });
|
|
const BROKEN_YAML = 'subpages:\n - page: faq\n bad: [unclosed\n';
|
|
|
|
describe('page rename when the menu cannot be updated', () => {
|
|
it('reports the rename as done, with a warning about the menu', async () => {
|
|
await writeFile(path.join(content, 'pages/faq.md'), md('FAQ'));
|
|
await writeFile(path.join(content, 'gallery.yaml'), BROKEN_YAML);
|
|
const { PUT } = await import('../pages/[slug]/route');
|
|
|
|
const res = await PUT(req('PUT', { rawMarkdown: md('FAQ'), newSlug: 'help' }), slugCtx('faq'));
|
|
|
|
expect(res.status).toBe(200);
|
|
const data = await res.json();
|
|
expect(data.success).toBe(true);
|
|
expect(data.page.slug).toBe('help');
|
|
expect(data.menuRenamed).toBe(false);
|
|
expect(data.galleryVersion).toBeNull();
|
|
expect(data.warning).toMatch(/gallery\.yaml/);
|
|
// The rename itself happened, and gallery.yaml was left alone.
|
|
await expect(readFile(path.join(content, 'pages/help.md'), 'utf8')).resolves.toBe(md('FAQ'));
|
|
await expect(readFile(path.join(content, 'pages/faq.md'))).rejects.toThrow();
|
|
await expect(readFile(path.join(content, 'gallery.yaml'), 'utf8')).resolves.toBe(BROKEN_YAML);
|
|
});
|
|
|
|
it('a plain save does not touch the menu and carries no warning', async () => {
|
|
await writeFile(path.join(content, 'pages/faq.md'), md('FAQ'));
|
|
await writeFile(path.join(content, 'gallery.yaml'), BROKEN_YAML);
|
|
const { PUT } = await import('../pages/[slug]/route');
|
|
|
|
const res = await PUT(req('PUT', { rawMarkdown: md('FAQ 2') }), slugCtx('faq'));
|
|
expect(res.status).toBe(200);
|
|
expect((await res.json()).warning).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('page delete when the menu cannot be updated', () => {
|
|
it('reports the delete as done, with a warning about the menu', async () => {
|
|
await writeFile(path.join(content, 'pages/faq.md'), md('FAQ'));
|
|
await writeFile(path.join(content, 'gallery.yaml'), BROKEN_YAML);
|
|
const { DELETE } = await import('../pages/[slug]/route');
|
|
|
|
const res = await DELETE(req('DELETE'), slugCtx('faq'));
|
|
|
|
expect(res.status).toBe(200);
|
|
const data = await res.json();
|
|
expect(data.success).toBe(true);
|
|
expect(data.deletedSlug).toBe('faq');
|
|
expect(data.removedFromMenu).toBe(false);
|
|
expect(data.galleryVersion).toBeNull();
|
|
expect(data.warning).toMatch(/gallery\.yaml/);
|
|
await expect(readFile(path.join(content, 'pages/faq.md'))).rejects.toThrow();
|
|
});
|
|
});
|