mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
ae80ad266d
Synology Photos, AirTrail and Dawarich each have a switch to trust a self-signed certificate; Immich had none, so every request to such a server failed with a bare "fetch failed" (#2475). Immich gets the same switch, per user and off by default: - users.immich_allow_insecure_tls (migration 243, column guarded) and a settings row labelled with the existing skipSSLVerification string, so no new translations. The settings card on desktop and phone draws it from that row. - Every request to the user's Immich (status, browse, search, asset info and proxy, albums, upload) passes rejectUnauthorized from the switch. Only 1 counts as on. A shared photo follows its owner's switch, since it goes to the owner's server with the owner's key. - Settings PUT and the connection test take allow_insecure_tls as a boolean. Absent on a save keeps the stored choice, so an older client cannot clear it; disconnecting turns it off again. - A failed request names its cause, e.g. "fetch failed (self-signed certificate)", instead of only the undici message. - The upload mirror gets a timeout: the journey upload waits on it. Fresh installs also never showed "Mirror journey photos to Immich on upload": migration 112 added that row only where the Immich provider already existed, and on a new database the seeds create it after the migrations. The row is now in the seeds, and migration 243 adds it where the provider exists without it. Wiki: Photo-Providers.md documents the switch.
210 lines
9.4 KiB
TypeScript
210 lines
9.4 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
// The MemoriesService is a thin pass-through over the legacy services/memories/*
|
|
// helpers. Mock each legacy module so we can assert the wrapper forwards every
|
|
// argument unchanged (and exercise the optional-param call sites).
|
|
|
|
const unified = vi.hoisted(() => ({
|
|
listTripPhotos: vi.fn(() => ({ data: [] })),
|
|
listTripAlbumLinks: vi.fn(() => ({ data: [] })),
|
|
createTripAlbumLink: vi.fn(() => ({ data: {} })),
|
|
removeAlbumLink: vi.fn(() => ({ data: {} })),
|
|
addTripPhotos: vi.fn(async () => ({ data: { added: 0 } })),
|
|
syncImmichAlbum: vi.fn(async () => ({ success: true, added: 0, total: 0 })),
|
|
syncSynologyAlbum: vi.fn(async () => ({ success: true, data: { added: 0, total: 0 } })),
|
|
removeTripPhoto: vi.fn(() => ({ data: {} })),
|
|
setTripPhotoSharing: vi.fn(async () => ({ data: {} })),
|
|
}));
|
|
|
|
const immich = vi.hoisted(() => ({
|
|
getConnectionSettings: vi.fn(() => ({})),
|
|
saveImmichSettings: vi.fn(async () => ({ success: true })),
|
|
setImmichAutoUpload: vi.fn(),
|
|
testConnection: vi.fn(async () => ({ connected: true })),
|
|
getConnectionStatus: vi.fn(async () => ({ connected: true })),
|
|
browseTimeline: vi.fn(async () => ({ buckets: [] })),
|
|
searchPhotos: vi.fn(async () => ({ assets: [] })),
|
|
streamImmichAsset: vi.fn(async () => undefined),
|
|
listAlbums: vi.fn(async () => ({ albums: [] })),
|
|
getAlbumPhotos: vi.fn(async () => ({ assets: [] })),
|
|
collectAlbumSelection: vi.fn(async () => ({ selection: { provider: 'immich', asset_ids: [] }, total: 0 })),
|
|
getAssetInfo: vi.fn(async () => ({ data: {} })),
|
|
isValidAssetId: vi.fn(() => true),
|
|
}));
|
|
|
|
const synology = vi.hoisted(() => ({
|
|
getSynologySettings: vi.fn(async () => ({ success: true, data: {} })),
|
|
updateSynologySettings: vi.fn(async () => ({ success: true, data: {} })),
|
|
getSynologyStatus: vi.fn(async () => ({ success: true, data: {} })),
|
|
testSynologyConnection: vi.fn(async () => ({ success: true, data: {} })),
|
|
listSynologyAlbums: vi.fn(async () => ({ success: true, data: {} })),
|
|
getSynologyAlbumPhotos: vi.fn(async () => ({ success: true, data: {} })),
|
|
collectSynologyAlbumSelection: vi.fn(async () => ({ success: true, data: { selection: { provider: 'synologyphotos', asset_ids: [] }, total: 0 } })),
|
|
searchSynologyPhotos: vi.fn(async () => ({ success: true, data: {} })),
|
|
getSynologyAssetInfo: vi.fn(async () => ({ success: true, data: {} })),
|
|
streamSynologyAsset: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
const helpers = vi.hoisted(() => ({ canAccessUserPhoto: vi.fn(() => true) }));
|
|
|
|
const ws = vi.hoisted(() => ({ broadcast: vi.fn() }));
|
|
vi.mock('../../../src/websocket', () => ws);
|
|
|
|
import { MemoriesService } from '../../../src/nest/memories/memories.service';
|
|
import { RealtimeService } from '../../../src/nest/realtime/realtime.service';
|
|
import type { UnifiedMemoriesService } from '../../../src/nest/memories/unified-memories.service';
|
|
import type { ImmichService } from '../../../src/nest/memories/immich.service';
|
|
import type { SynologyService } from '../../../src/nest/memories/synology.service';
|
|
import type { MemoriesAccessService } from '../../../src/nest/memories/memories-access.service';
|
|
|
|
const res = {} as import('express').Response;
|
|
|
|
describe('MemoriesService (delegation wrapper over services/memories/*)', () => {
|
|
let svc: MemoriesService;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
svc = new MemoriesService(
|
|
new RealtimeService(),
|
|
unified as unknown as UnifiedMemoriesService,
|
|
immich as unknown as ImmichService,
|
|
synology as unknown as SynologyService,
|
|
helpers as unknown as MemoriesAccessService,
|
|
);
|
|
});
|
|
|
|
it('access check + broadcast forward verbatim', () => {
|
|
helpers.canAccessUserPhoto.mockReturnValue(false);
|
|
expect(svc.canAccessUserPhoto(1, 2, '5', 'a', 'immich')).toBe(false);
|
|
expect(helpers.canAccessUserPhoto).toHaveBeenCalledWith(1, 2, '5', 'a', 'immich');
|
|
|
|
svc.broadcast('5', 'memories:updated', { userId: 1 }, 'sock');
|
|
expect(ws.broadcast).toHaveBeenCalledWith('5', 'memories:updated', { userId: 1 }, 'sock');
|
|
});
|
|
|
|
it('broadcast forwards an absent socket id as undefined', () => {
|
|
svc.broadcast('5', 'memories:updated', { userId: 1 });
|
|
expect(ws.broadcast).toHaveBeenCalledWith('5', 'memories:updated', { userId: 1 }, undefined);
|
|
});
|
|
|
|
it('unified methods delegate', async () => {
|
|
svc.listTripPhotos('5', 7);
|
|
expect(unified.listTripPhotos).toHaveBeenCalledWith('5', 7);
|
|
|
|
const selections = [{ provider: 'immich', asset_ids: ['a'] }];
|
|
await svc.addTripPhotos('5', 7, true, selections, 'sock');
|
|
expect(unified.addTripPhotos).toHaveBeenCalledWith('5', 7, true, selections, 'sock');
|
|
|
|
await svc.setTripPhotoSharing('5', 7, 9, false);
|
|
expect(unified.setTripPhotoSharing).toHaveBeenCalledWith('5', 7, 9, false);
|
|
|
|
svc.removeTripPhoto('5', 7, 9);
|
|
expect(unified.removeTripPhoto).toHaveBeenCalledWith('5', 7, 9);
|
|
|
|
svc.listTripAlbumLinks('5', 7);
|
|
expect(unified.listTripAlbumLinks).toHaveBeenCalledWith('5', 7);
|
|
|
|
svc.removeAlbumLink('5', 'l1', 7);
|
|
expect(unified.removeAlbumLink).toHaveBeenCalledWith('5', 'l1', 7);
|
|
});
|
|
|
|
it('createTripAlbumLink forwards a passphrase when present and omits it when absent', () => {
|
|
svc.createTripAlbumLink('5', 7, 'immich', 'a1', 'Trip', 'secret');
|
|
expect(unified.createTripAlbumLink).toHaveBeenCalledWith('5', 7, 'immich', 'a1', 'Trip', 'secret');
|
|
|
|
svc.createTripAlbumLink('5', 7, 'immich', 'a1', 'Trip');
|
|
expect(unified.createTripAlbumLink).toHaveBeenLastCalledWith('5', 7, 'immich', 'a1', 'Trip', undefined);
|
|
});
|
|
|
|
it('immich methods delegate', async () => {
|
|
svc.immichGetConnectionSettings(7);
|
|
expect(immich.getConnectionSettings).toHaveBeenCalledWith(7);
|
|
|
|
await svc.immichSaveSettings(7, 'u', 'k', '1.2.3.4', true);
|
|
expect(immich.saveImmichSettings).toHaveBeenCalledWith(7, 'u', 'k', '1.2.3.4', true);
|
|
|
|
svc.immichSetAutoUpload(7, true);
|
|
expect(immich.setImmichAutoUpload).toHaveBeenCalledWith(7, true);
|
|
|
|
await svc.immichGetConnectionStatus(7);
|
|
expect(immich.getConnectionStatus).toHaveBeenCalledWith(7);
|
|
|
|
await svc.immichTestConnection('u', 'k', true);
|
|
expect(immich.testConnection).toHaveBeenCalledWith('u', 'k', true);
|
|
|
|
await svc.immichBrowseTimeline(7);
|
|
expect(immich.browseTimeline).toHaveBeenCalledWith(7);
|
|
|
|
await svc.immichSearchPhotos(7, 'f', 't', 2, 50);
|
|
expect(immich.searchPhotos).toHaveBeenCalledWith(7, 'f', 't', 2, 50);
|
|
|
|
expect(svc.immichIsValidAssetId('abc')).toBe(true);
|
|
expect(immich.isValidAssetId).toHaveBeenCalledWith('abc');
|
|
|
|
await svc.immichGetAssetInfo(7, 'a', 2);
|
|
expect(immich.getAssetInfo).toHaveBeenCalledWith(7, 'a', 2);
|
|
|
|
await svc.immichStreamAsset(res, 7, 'a', 'thumbnail', 2);
|
|
expect(immich.streamImmichAsset).toHaveBeenCalledWith(res, 7, 'a', 'thumbnail', 2);
|
|
|
|
await svc.immichListAlbums(7);
|
|
expect(immich.listAlbums).toHaveBeenCalledWith(7);
|
|
|
|
await svc.immichGetAlbumPhotos(7, 'al1');
|
|
expect(immich.getAlbumPhotos).toHaveBeenCalledWith(7, 'al1');
|
|
|
|
await svc.immichSyncAlbumAssets('5', 'l1', 7, 'sock');
|
|
expect(unified.syncImmichAlbum).toHaveBeenCalledWith('5', 'l1', 7, 'sock');
|
|
});
|
|
|
|
it('synology methods delegate', async () => {
|
|
await svc.synologyGetSettings(7);
|
|
expect(synology.getSynologySettings).toHaveBeenCalledWith(7);
|
|
|
|
await svc.synologyUpdateSettings(7, 'u', 'a', 'p', true);
|
|
expect(synology.updateSynologySettings).toHaveBeenCalledWith(7, 'u', 'a', 'p', true);
|
|
|
|
await svc.synologyGetStatus(7);
|
|
expect(synology.getSynologyStatus).toHaveBeenCalledWith(7);
|
|
|
|
await svc.synologyTestConnection(7, 'u', 'a', 'p', '123', false);
|
|
expect(synology.testSynologyConnection).toHaveBeenCalledWith(7, 'u', 'a', 'p', '123', false);
|
|
|
|
await svc.synologyListAlbums(7);
|
|
expect(synology.listSynologyAlbums).toHaveBeenCalledWith(7);
|
|
|
|
await svc.synologySyncAlbumLink(7, '5', 'l1', 'sock');
|
|
expect(unified.syncSynologyAlbum).toHaveBeenCalledWith(7, '5', 'l1', 'sock');
|
|
|
|
await svc.synologySearchPhotos(7, 'f', 't', 0, 100);
|
|
expect(synology.searchSynologyPhotos).toHaveBeenCalledWith(7, 'f', 't', 0, 100, 0);
|
|
|
|
// The zone the days are meant in has to reach the provider, and it is a
|
|
// different thing from the pagination offset sitting next to it (#2336).
|
|
await svc.synologySearchPhotos(7, 'f', 't', 0, 100, 600);
|
|
expect(synology.searchSynologyPhotos).toHaveBeenLastCalledWith(7, 'f', 't', 0, 100, 600);
|
|
});
|
|
|
|
it('synology album-photos forwards a passphrase when present and omits it when absent', async () => {
|
|
await svc.synologyGetAlbumPhotos(7, 'al1', 'secret');
|
|
expect(synology.getSynologyAlbumPhotos).toHaveBeenCalledWith(7, 'al1', 'secret');
|
|
|
|
await svc.synologyGetAlbumPhotos(7, 'al1');
|
|
expect(synology.getSynologyAlbumPhotos).toHaveBeenLastCalledWith(7, 'al1', undefined);
|
|
});
|
|
|
|
it('synology asset-info + stream forward a passphrase when present and omit it when absent', async () => {
|
|
await svc.synologyGetAssetInfo(7, 'p1', 2, 'secret');
|
|
expect(synology.getSynologyAssetInfo).toHaveBeenCalledWith(7, 'p1', 2, 'secret');
|
|
|
|
await svc.synologyGetAssetInfo(7, 'p1', 2);
|
|
expect(synology.getSynologyAssetInfo).toHaveBeenLastCalledWith(7, 'p1', 2, undefined);
|
|
|
|
await svc.synologyStreamAsset(res, 7, 2, 'p1', 'thumbnail', 'sm', 'secret');
|
|
expect(synology.streamSynologyAsset).toHaveBeenCalledWith(res, 7, 2, 'p1', 'thumbnail', 'sm', 'secret');
|
|
|
|
await svc.synologyStreamAsset(res, 7, 2, 'p1', 'original', 'xl');
|
|
expect(synology.streamSynologyAsset).toHaveBeenLastCalledWith(res, 7, 2, 'p1', 'original', 'xl', undefined);
|
|
});
|
|
});
|