Files
immich-folio/lib/__tests__/immichCache.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

108 lines
3.7 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
// vi.mock is hoisted above the file, so the stubs have to be too.
const { getStale, set } = vi.hoisted(() => ({ getStale: vi.fn(), set: vi.fn() }));
vi.mock('../cache', () => ({ cache: { getStale, set } }));
import { MISSING, cacheSet, staleOrMissing, staleOrThrow } from '../immichCache';
import { ImmichUnavailableError } from '../immichTransport';
/**
* This policy decides what visitors see while Immich is down, and it used to be
* reachable only through HTTP mocks against the client (#610). These are the
* tests that became possible once it stood on its own.
*/
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, 'warn').mockImplementation(() => {});
});
describe('staleOrThrow', () => {
it('serves the last known good answer when Immich is unavailable', () => {
getStale.mockReturnValue([{ id: 'album-1' }]);
const result = staleOrThrow('albums', new ImmichUnavailableError('502'), 'albums');
expect(result).toEqual([{ id: 'album-1' }]);
});
it('rethrows when nothing usable is left in the cache', () => {
// Past staleMaxAge the entry is gone, and the outage surfaces as before.
getStale.mockReturnValue(null);
expect(() => staleOrThrow('albums', new ImmichUnavailableError('502'), 'albums')).toThrow(
ImmichUnavailableError,
);
});
it('does not serve stale data for an error that is not an outage', () => {
// A TypeError means we asked the wrong question. Answering it with
// yesterday's data would hide a real bug behind a plausible page.
getStale.mockReturnValue([{ id: 'album-1' }]);
expect(() => staleOrThrow('albums', new TypeError('read of undefined'), 'albums')).toThrow(
TypeError,
);
expect(getStale).not.toHaveBeenCalled();
});
it('says so in the log when it serves something stale', () => {
getStale.mockReturnValue(['cached']);
staleOrThrow('albums', new ImmichUnavailableError('502'), 'album list');
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('album list'));
});
});
describe('the MISSING sentinel (#626)', () => {
it('is never cast into real data by staleOrThrow', () => {
// The sentinel handed back as an album is a truthy object with no fields;
// every `if (!album)` guard passes and the page answers 500. For a key
// that should only ever hold data, meeting it means nothing is usable.
getStale.mockReturnValue(MISSING);
expect(() => staleOrThrow('albums', new ImmichUnavailableError('502'), 'albums')).toThrow(
ImmichUnavailableError,
);
});
it('comes back from staleOrMissing as null — the last authoritative answer', () => {
getStale.mockReturnValue(MISSING);
expect(staleOrMissing('asset-x', new ImmichUnavailableError('502'), 'asset x')).toBeNull();
});
it('lets staleOrMissing still serve real stale data', () => {
getStale.mockReturnValue({ id: 'asset-x' });
expect(staleOrMissing('asset-x', new ImmichUnavailableError('502'), 'asset x')).toEqual({
id: 'asset-x',
});
});
it('lets staleOrMissing throw when nothing is cached', () => {
getStale.mockReturnValue(null);
expect(() => staleOrMissing('asset-x', new ImmichUnavailableError('502'), 'asset x')).toThrow(
ImmichUnavailableError,
);
});
it('keeps staleOrMissing from answering a non-outage error', () => {
getStale.mockReturnValue(MISSING);
expect(() => staleOrMissing('asset-x', new TypeError('bug'), 'asset x')).toThrow(TypeError);
expect(getStale).not.toHaveBeenCalled();
});
});
describe('cacheSet', () => {
it('passes the configured windows through', () => {
cacheSet('albums', ['a'], 60_000, 3_600_000);
expect(set).toHaveBeenCalledWith('albums', ['a'], 60_000, 3_600_000);
});
});