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:
107
lib/__tests__/immichCache.test.ts
Normal file
107
lib/__tests__/immichCache.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user