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>
This commit is contained in:
Ralksta
2026-09-22 14:12:13 +02:00
committed by GitHub
parent c3f92ca4ec
commit a3a6319c29
120 changed files with 6818 additions and 975 deletions

View File

@@ -4,6 +4,7 @@ import {
checkProxyHops,
countForwardedHops,
checkAlbumIds,
checkAlbumSlugCollisions,
checkAlbumsShared,
checkPasswords,
checkWritable,
@@ -11,6 +12,14 @@ import {
worstLevel,
} from '../admin/doctor';
/** A stand-in for lib/config/schema.ts's slugify(), good enough to test the
* grouping logic without doctor.ts importing anything (see its own comment). */
const testSlug = (name: string) =>
name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
describe('checkAuthSecret', () => {
it('accepts a long secret', () => {
expect(checkAuthSecret('a'.repeat(64)).level).toBe('ok');
@@ -94,6 +103,19 @@ describe('checkProxyHops', () => {
it('accepts one configured hop against one corroborated entry', () => {
expect(checkProxyHops(1, 1, true).level).toBe('ok');
});
/**
* Cloudflare (1 hop) in front of nginx (1 more, 2 total) with the value set
* to 1 used to report "matches the observed chain" — the client IP was
* actually read from the Cloudflare edge address, so every visitor behind
* that PoP shared one rate-limit bucket (#629).
*/
it('warns, not passes, when fewer hops are configured than arrived', () => {
const f = checkProxyHops(1, 2, true);
expect(f.level).toBe('warn');
expect(f.title).toContain('1');
expect(f.title).toContain('2');
});
});
describe('checkAlbumIds', () => {
@@ -142,6 +164,71 @@ describe('checkAlbumsShared', () => {
});
});
describe('checkAlbumSlugCollisions', () => {
const known = [
{ id: 'a', albumName: 'Japan Trip' },
{ id: 'b', albumName: 'Japan-Trip' },
{ id: 'c', albumName: 'Iceland' },
];
it('is quiet when nothing collides', () => {
const f = checkAlbumSlugCollisions(
[{ context: 'gallery.yaml albums', albumIds: ['a', 'c'] }],
{},
known,
testSlug,
);
expect(f.level).toBe('ok');
});
it('flags two albums in the same group whose Immich names slugify alike', () => {
const f = checkAlbumSlugCollisions(
[{ context: 'gallery.yaml albums', albumIds: ['a', 'b'] }],
{},
known,
testSlug,
);
expect(f.level).toBe('error');
expect(f.detail).toContain('Japan Trip');
expect(f.detail).toContain('Japan-Trip');
expect(f.albumIds).toEqual(['a', 'b']);
});
it('does not flag the same collision across two different groups', () => {
// /trips-a/japan-trip and /trips-b/japan-trip are different URLs.
const f = checkAlbumSlugCollisions(
[
{ context: 'subpage "Trips A"', albumIds: ['a'] },
{ context: 'subpage "Trips B"', albumIds: ['b'] },
],
{},
known,
testSlug,
);
expect(f.level).toBe('ok');
});
it('prefers a title override over the Immich name', () => {
const f = checkAlbumSlugCollisions(
[{ context: 'gallery.yaml albums', albumIds: ['a', 'c'] }],
{ c: 'Japan Trip' }, // overrides "Iceland" — now collides with "a"
known,
testSlug,
);
expect(f.level).toBe('error');
});
it('says nothing about an id Immich does not know and has no override', () => {
const f = checkAlbumSlugCollisions(
[{ context: 'gallery.yaml albums', albumIds: ['a', 'ghost'] }],
{},
known,
testSlug,
);
expect(f.level).toBe('ok');
});
});
describe('checkPasswords', () => {
it('passes when everything is hashed', () => {
expect(checkPasswords([{ label: 'Site', value: 'scrypt:aa:bb' }]).level).toBe('ok');