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:
Ralksta
2026-09-29 22:50:30 +02:00
committed by GitHub
parent 5f251923f0
commit ad8b09700b
297 changed files with 19398 additions and 1800 deletions

View File

@@ -0,0 +1,76 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import { buildWebhookPayload, notifySubmitted, webhookUrl } from '../proofWebhook';
import type { ProofSession } from '../proofSessions';
const session: ProofSession = {
id: 'abc123',
token: 'x'.repeat(32),
clientName: 'Anna & Ben',
albumId: '11111111-1111-1111-1111-111111111111',
createdAt: '2026-01-01T00:00:00Z',
download: 'none',
downloadsUsed: 0,
selection: ['a', 'b'],
submittedAt: '2026-02-01T00:00:00Z',
};
afterEach(() => {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
describe('webhookUrl', () => {
it('accepts http(s) only', () => {
expect(webhookUrl('https://ntfy.example/x')).toBe('https://ntfy.example/x');
expect(webhookUrl('ftp://x')).toBeNull();
expect(webhookUrl('not a url')).toBeNull();
expect(webhookUrl('')).toBeNull();
});
});
describe('buildWebhookPayload', () => {
it('speaks Discord, Slack and Gotify at once, and never carries the client token', () => {
const payload = buildWebhookPayload(session, 'Wedding', 'https://folio.example');
expect(payload.content).toContain('Anna & Ben');
expect(payload.text).toContain('2 photos');
expect(payload.title).toBeTruthy();
expect(payload.message).toContain('"Wedding"');
expect(payload.proofing.adminUrl).toBe('https://folio.example/admin/proofing?session=abc123');
expect(JSON.stringify(payload)).not.toContain(session.token);
});
it('works without a site URL', () => {
expect(buildWebhookPayload(session, 'Wedding', null).proofing.adminUrl).toBeNull();
});
});
describe('notifySubmitted', () => {
it('does nothing without a URL', async () => {
const fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
await notifySubmitted(session, 'Wedding', null);
expect(fetchMock).not.toHaveBeenCalled();
});
it('posts JSON to the configured URL', async () => {
vi.stubEnv('PROOFING_WEBHOOK_URL', 'https://hooks.example/abc');
const fetchMock = vi.fn(async () => new Response('ok'));
vi.stubGlobal('fetch', fetchMock);
await notifySubmitted(session, 'Wedding', null);
const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit];
expect(url).toBe('https://hooks.example/abc');
expect(JSON.parse(init.body as string).event).toBe('proofing.submitted');
});
it('swallows a failing webhook', async () => {
vi.stubEnv('PROOFING_WEBHOOK_URL', 'https://hooks.example/abc');
vi.stubGlobal(
'fetch',
vi.fn(async () => Promise.reject(new Error('down'))),
);
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
await expect(notifySubmitted(session, 'Wedding', null)).resolves.toBeUndefined();
expect(warn).toHaveBeenCalled();
});
});