Files
immich-folio/lib/__tests__/settings-schema.test.ts
Ralksta a3a6319c29 release: v0.16.0
See CHANGELOG.md for the full v0.16.0 entry.

---------

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>
2026-09-22 14:12:13 +02:00

125 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import { validateSettings } from '../config/settingsSchema';
/**
* Half of these tests are about what must *not* be rejected.
*
* The failure mode of a validator on a config file is not "it let something
* through" — it is "it locked an operator out of their own settings, through
* the panel that writes the file". There is no schemaVersion and no migration
* code, so older and newer configurations are both in the wild.
*/
const ok = (input: unknown) => validateSettings(input).ok;
const errors = (input: unknown) => {
const result = validateSettings(input);
return result.ok ? [] : result.errors;
};
describe('validateSettings — accepts what works today', () => {
it('accepts the example settings file this repo ships with', () => {
// content/settings.yaml is gitignored — the example is the one every
// installation starts from, and the one CI actually has.
const raw = fs.readFileSync(path.join(process.cwd(), 'content/settings.yaml.example'), 'utf8');
expect(validateSettings(yaml.load(raw))).toEqual({ ok: true });
});
it('accepts an empty settings file', () => {
// Everything has a default; a site with no settings.yaml is a valid site.
expect(ok({})).toBe(true);
});
it('accepts both spellings of theme', () => {
expect(ok({ theme: 'minimal' })).toBe(true);
expect(ok({ theme: { preset: 'minimal', accent: '#c0ffee', radius: 4 } })).toBe(true);
});
it('accepts a watermark opacity written as a percentage', () => {
// The #508 workaround: values above 1 are read as percentages. A validator
// that insisted on 0–1 would reject configurations that render correctly.
expect(ok({ watermark: { opacity: 90 } })).toBe(true);
expect(ok({ watermark: { opacity: 0.5 } })).toBe(true);
});
it('accepts a colour mode it does not recognise', () => {
// resolveColorMode falls back to 'dark'. Rejecting here would be stricter
// than the behaviour it guards.
expect(ok({ mode: 'sepia' })).toBe(true);
});
it('accepts keys this version has never heard of', () => {
expect(ok({ title: 'Folio', somethingFromANewerVersion: { nested: true } })).toBe(true);
expect(ok({ theme: { preset: 'noir', futureKnob: 3 } })).toBe(true);
});
it('accepts the legacy exifOnHover switch alongside the group form', () => {
expect(ok({ exifOnHover: false, exif: { camera: true, caption: false } })).toBe(true);
});
});
describe('validateSettings — rejects what breaks the site', () => {
it('rejects a payload that is not an object', () => {
expect(ok('title: Folio')).toBe(false);
expect(ok([{ title: 'Folio' }])).toBe(false);
expect(ok(null)).toBe(false);
});
it('rejects a scalar where a section belongs', () => {
// `grid: 3` reads as "three columns" to a human and as a crash to
// buildCoverGridVars.
const found = errors({ grid: 3 });
expect(found).toHaveLength(1);
expect(found[0].field).toBe('grid');
});
it('rejects a section where a scalar belongs', () => {
const found = errors({ title: { de: 'Folio', en: 'Folio' } });
expect(found[0].field).toBe('title');
});
it('names the exact field, however deep', () => {
const found = errors({ theme: { fonts: { heading: 42 } } });
expect(found.map((e) => e.field)).toContain('theme.fonts.heading');
});
it('rejects a theme that is neither a preset name nor a section', () => {
// The separate theme check must not turn into a hole: anything that is not
// a string still has to look like the object form.
expect(ok({ theme: 3 })).toBe(false);
expect(ok({ theme: ['minimal'] })).toBe(false);
expect(errors({ theme: 3 })[0].field).toBe('theme');
});
it('names an index inside navLinks', () => {
const found = errors({ navLinks: [{ label: 'Shop', url: 'https://example.com' }, 'nope'] });
expect(found[0].field).toBe('navLinks.1');
});
it('rejects a radius written with its unit', () => {
// #633: `radius: 8px` reaches CSS as `8pxpx`. The value itself belongs to
// the resolver, but a string where a number belongs is a type error, and
// that half is caught here for anything saved from the panel.
const found = errors({ theme: { radius: '8px' } });
expect(found.map((e) => e.field)).toContain('theme.radius');
});
it('rejects a list where a record belongs', () => {
expect(ok({ legal: ['Ralf', 'Berlin'] })).toBe(false);
});
it('reports every offending field at once, not just the first', () => {
const found = errors({ title: 1, subtitle: 2, map: 'yes' });
expect(found.map((e) => e.field).sort()).toEqual(['map', 'subtitle', 'title']);
});
});