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:
@@ -23,6 +23,15 @@ vi.mock('@/lib/env', () => ({
|
||||
return process.env.INSTALL_CONTENT_DIR || '';
|
||||
},
|
||||
},
|
||||
// Real implementation, not a stub: install.ts's own normalisation tests
|
||||
// (#634) rely on this actually stripping trailing slashes.
|
||||
normalizeApiUrl: (raw: string) => {
|
||||
try {
|
||||
return new URL(raw).toString().replace(/\/+$/, '');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
/** Import fresh so the module-level install-file cache cannot leak between cases. */
|
||||
@@ -103,6 +112,45 @@ describe('getInstallCredentials', () => {
|
||||
adminPassword: '',
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The environment path already strips a trailing slash (lib/env.ts); the
|
||||
* file path used to store content/install.json's apiUrl completely
|
||||
* unnormalised. `https://host/api/` then reached getConfig() as
|
||||
* `.../api/`, which does not `endsWith('/api')`, so it became
|
||||
* `.../api//api` and every request 404ed (#634). Normalising on every read
|
||||
* means an install.json written before this fix heals without re-running
|
||||
* the wizard.
|
||||
*/
|
||||
describe('heals an unnormalised URL already on disk (#634)', () => {
|
||||
it.each([
|
||||
['trailing slash, no /api', 'https://file-immich:2283/', 'https://file-immich:2283'],
|
||||
[
|
||||
'trailing slash, with /api',
|
||||
'https://file-immich:2283/api/',
|
||||
'https://file-immich:2283/api',
|
||||
],
|
||||
['no trailing slash, already clean', 'https://file-immich:2283', 'https://file-immich:2283'],
|
||||
])('file path: %s', async (_name, stored, expected) => {
|
||||
const { getInstallCredentials } = await loadInstall();
|
||||
fs.writeFileSync(
|
||||
path.join(dir, 'install.json'),
|
||||
JSON.stringify({ apiUrl: stored, apiKey: 'file-key' }),
|
||||
);
|
||||
|
||||
expect(getInstallCredentials().apiUrl).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['trailing slash, no /api', 'https://env-immich:2283/', 'https://env-immich:2283'],
|
||||
['trailing slash, with /api', 'https://env-immich:2283/api/', 'https://env-immich:2283/api'],
|
||||
])('env path: %s', async (_name, envValue, expected) => {
|
||||
const { getInstallCredentials } = await loadInstall();
|
||||
process.env.__T_INSTALL_URL = envValue;
|
||||
|
||||
expect(getInstallCredentials().apiUrl).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInstalled', () => {
|
||||
@@ -168,6 +216,23 @@ describe('completeInstall', () => {
|
||||
expect(getInstallCredentials().authSecret).toBe(creds.authSecret);
|
||||
});
|
||||
|
||||
/**
|
||||
* The route verifies the connection against normalizeApiBase(apiUrl),
|
||||
* which strips a trailing slash, but this used to write input.apiUrl.trim()
|
||||
* unchanged — so a URL that passed the ping test was stored as typed and
|
||||
* every request afterwards 404ed (#634).
|
||||
*/
|
||||
it.each([
|
||||
['trailing slash, no /api', 'https://immich.example/', 'https://immich.example'],
|
||||
['trailing slash, with /api', 'https://immich.example/api/', 'https://immich.example/api'],
|
||||
])('normalises the stored apiUrl: %s', async (_name, typed, expected) => {
|
||||
const { completeInstall } = await loadInstall();
|
||||
await completeInstall({ apiUrl: typed, apiKey: 'api-key-123' });
|
||||
|
||||
const creds = JSON.parse(fs.readFileSync(path.join(dir, 'install.json'), 'utf8'));
|
||||
expect(creds.apiUrl).toBe(expected);
|
||||
});
|
||||
|
||||
it('writes an empty albums list when no albums are selected', async () => {
|
||||
const { completeInstall } = await loadInstall();
|
||||
await completeInstall({
|
||||
@@ -202,6 +267,68 @@ describe('completeInstall', () => {
|
||||
|
||||
expect(isInstalled()).toBe(true);
|
||||
});
|
||||
|
||||
/**
|
||||
* isInstalled() is false whenever Immich credentials fail to resolve,
|
||||
* whether or not gallery.yaml exists — a bad IMMICH_API_URL or a corrupt
|
||||
* install.json is enough. Re-running the wizard after that used to replace
|
||||
* every subpage, section, password, assetOrder and per-album override with
|
||||
* a two-key skeleton (#627).
|
||||
*/
|
||||
it('does not overwrite an existing gallery.yaml', async () => {
|
||||
const { completeInstall } = await loadInstall();
|
||||
const galleryPath = path.join(dir, 'gallery.yaml');
|
||||
const existing = 'albums:\n - existing-album\nsubpages:\n - slug: trips\n';
|
||||
fs.writeFileSync(galleryPath, existing);
|
||||
|
||||
const result = await completeInstall({
|
||||
apiUrl: 'https://immich.example',
|
||||
apiKey: 'api-key-123',
|
||||
albums: [ALBUM_ID],
|
||||
});
|
||||
|
||||
expect(result.galleryWritten).toBe(false);
|
||||
expect(fs.readFileSync(galleryPath, 'utf8')).toBe(existing);
|
||||
});
|
||||
|
||||
it('does not overwrite an existing settings.yaml', async () => {
|
||||
const { completeInstall } = await loadInstall();
|
||||
const settingsPath = path.join(dir, 'settings.yaml');
|
||||
const existing = 'title: "Existing Site"\n';
|
||||
fs.writeFileSync(settingsPath, existing);
|
||||
|
||||
const result = await completeInstall({
|
||||
apiUrl: 'https://immich.example',
|
||||
apiKey: 'api-key-123',
|
||||
siteTitle: 'New Title',
|
||||
});
|
||||
|
||||
expect(result.settingsWritten).toBe(false);
|
||||
expect(fs.readFileSync(settingsPath, 'utf8')).toBe(existing);
|
||||
});
|
||||
|
||||
it('writes both files on a genuinely fresh install and reports it', async () => {
|
||||
const { completeInstall } = await loadInstall();
|
||||
|
||||
const result = await completeInstall({
|
||||
apiUrl: 'https://immich.example',
|
||||
apiKey: 'api-key-123',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ galleryWritten: true, settingsWritten: true });
|
||||
});
|
||||
|
||||
it('still writes fresh credentials into install.json when gallery.yaml is kept', async () => {
|
||||
const { completeInstall, getInstallCredentials } = await loadInstall();
|
||||
fs.writeFileSync(path.join(dir, 'gallery.yaml'), 'albums: []\n');
|
||||
|
||||
await completeInstall({
|
||||
apiUrl: 'https://immich.example',
|
||||
apiKey: 'fresh-key',
|
||||
});
|
||||
|
||||
expect(getInstallCredentials().apiKey).toBe('fresh-key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeApiBase', () => {
|
||||
|
||||
Reference in New Issue
Block a user