mirror of
https://github.com/immich-app/immich.git
synced 2026-03-03 02:37:02 +00:00
Compare commits
3 Commits
d94d9600a7
...
push-vxwxq
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf139fb7b3 | ||
|
|
55e625a2ac | ||
|
|
ca6c486a80 |
@@ -20,6 +20,9 @@ const config: PlaywrightTestConfig = {
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 4 : 0,
|
||||
reporter: 'html',
|
||||
expect: {
|
||||
timeout: 15_000,
|
||||
},
|
||||
use: {
|
||||
baseURL: playwriteBaseUrl,
|
||||
trace: 'on-first-retry',
|
||||
|
||||
167
e2e/src/ui/mock-network/broken-asset-network.ts
Normal file
167
e2e/src/ui/mock-network/broken-asset-network.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { AssetTypeEnum, AssetVisibility, type AssetResponseDto, type StackResponseDto } from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
import { randomPreview, randomThumbnail } from 'src/ui/generators/timeline';
|
||||
|
||||
export type MockStack = {
|
||||
id: string;
|
||||
primaryAssetId: string;
|
||||
assets: AssetResponseDto[];
|
||||
brokenAssetIds: Set<string>;
|
||||
assetMap: Map<string, AssetResponseDto>;
|
||||
};
|
||||
|
||||
export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
|
||||
const assetId = faker.string.uuid();
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: assetId,
|
||||
deviceAssetId: `device-${assetId}`,
|
||||
ownerId,
|
||||
owner: {
|
||||
id: ownerId,
|
||||
email: 'admin@immich.cloud',
|
||||
name: 'Admin',
|
||||
profileImagePath: '',
|
||||
profileChangedAt: now,
|
||||
avatarColor: 'blue' as never,
|
||||
},
|
||||
libraryId: `library-${ownerId}`,
|
||||
deviceId: `device-${ownerId}`,
|
||||
type: AssetTypeEnum.Image,
|
||||
originalPath: `/original/${assetId}.jpg`,
|
||||
originalFileName: `${assetId}.jpg`,
|
||||
originalMimeType: 'image/jpeg',
|
||||
thumbhash: null,
|
||||
fileCreatedAt: now,
|
||||
fileModifiedAt: now,
|
||||
localDateTime: now,
|
||||
updatedAt: now,
|
||||
createdAt: now,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
duration: '0:00:00.00000',
|
||||
exifInfo: {
|
||||
make: null,
|
||||
model: null,
|
||||
exifImageWidth: 3000,
|
||||
exifImageHeight: 4000,
|
||||
fileSizeInByte: null,
|
||||
orientation: null,
|
||||
dateTimeOriginal: now,
|
||||
modifyDate: null,
|
||||
timeZone: null,
|
||||
lensModel: null,
|
||||
fNumber: null,
|
||||
focalLength: null,
|
||||
iso: null,
|
||||
exposureTime: null,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
city: null,
|
||||
country: null,
|
||||
state: null,
|
||||
description: null,
|
||||
},
|
||||
livePhotoVideoId: null,
|
||||
tags: [],
|
||||
people: [],
|
||||
unassignedFaces: [],
|
||||
stack: null,
|
||||
isOffline: false,
|
||||
hasMetadata: true,
|
||||
duplicateId: null,
|
||||
resized: true,
|
||||
checksum: faker.string.alphanumeric({ length: 28 }),
|
||||
width: 3000,
|
||||
height: 4000,
|
||||
isEdited: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockStack = (
|
||||
primaryAssetDto: AssetResponseDto,
|
||||
additionalAssets: AssetResponseDto[],
|
||||
brokenAssetIds?: Set<string>,
|
||||
): MockStack => {
|
||||
const stackId = faker.string.uuid();
|
||||
const allAssets = [primaryAssetDto, ...additionalAssets];
|
||||
const resolvedBrokenIds = brokenAssetIds ?? new Set(additionalAssets.map((a) => a.id));
|
||||
const assetMap = new Map(allAssets.map((a) => [a.id, a]));
|
||||
|
||||
primaryAssetDto.stack = {
|
||||
id: stackId,
|
||||
assetCount: allAssets.length,
|
||||
primaryAssetId: primaryAssetDto.id,
|
||||
};
|
||||
|
||||
return {
|
||||
id: stackId,
|
||||
primaryAssetId: primaryAssetDto.id,
|
||||
assets: allAssets,
|
||||
brokenAssetIds: resolvedBrokenIds,
|
||||
assetMap,
|
||||
};
|
||||
};
|
||||
|
||||
export const setupBrokenAssetMockApiRoutes = async (context: BrowserContext, mockStack: MockStack) => {
|
||||
await context.route('**/api/stacks/*', async (route, request) => {
|
||||
if (request.method() !== 'GET') {
|
||||
return route.fallback();
|
||||
}
|
||||
const stackResponse: StackResponseDto = {
|
||||
id: mockStack.id,
|
||||
primaryAssetId: mockStack.primaryAssetId,
|
||||
assets: mockStack.assets,
|
||||
};
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: stackResponse,
|
||||
});
|
||||
});
|
||||
|
||||
await context.route('**/api/assets/*', async (route, request) => {
|
||||
if (request.method() !== 'GET') {
|
||||
return route.fallback();
|
||||
}
|
||||
const url = new URL(request.url());
|
||||
const segments = url.pathname.split('/');
|
||||
const assetId = segments.at(-1);
|
||||
if (assetId && mockStack.assetMap.has(assetId)) {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: mockStack.assetMap.get(assetId),
|
||||
});
|
||||
}
|
||||
return route.fallback();
|
||||
});
|
||||
|
||||
await context.route('**/api/assets/*/thumbnail?size=*', async (route, request) => {
|
||||
if (!route.request().serviceWorker()) {
|
||||
return route.continue();
|
||||
}
|
||||
const pattern = /\/api\/assets\/(?<assetId>[^/]+)\/thumbnail\?size=(?<size>preview|thumbnail)/;
|
||||
const match = request.url().match(pattern);
|
||||
if (!match?.groups || !mockStack.assetMap.has(match.groups.assetId)) {
|
||||
return route.fallback();
|
||||
}
|
||||
if (mockStack.brokenAssetIds.has(match.groups.assetId)) {
|
||||
return route.fulfill({ status: 404 });
|
||||
}
|
||||
const asset = mockStack.assetMap.get(match.groups.assetId)!;
|
||||
const ratio = (asset.exifInfo?.exifImageWidth ?? 3000) / (asset.exifInfo?.exifImageHeight ?? 4000);
|
||||
const body =
|
||||
match.groups.size === 'preview'
|
||||
? await randomPreview(match.groups.assetId, ratio)
|
||||
: await randomThumbnail(match.groups.assetId, ratio);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'image/jpeg' },
|
||||
body,
|
||||
});
|
||||
});
|
||||
};
|
||||
84
e2e/src/ui/specs/asset-viewer/broken-asset.e2e-spec.ts
Normal file
84
e2e/src/ui/specs/asset-viewer/broken-asset.e2e-spec.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { toAssetResponseDto } from 'src/ui/generators/timeline';
|
||||
import {
|
||||
createMockStack,
|
||||
createMockStackAsset,
|
||||
MockStack,
|
||||
setupBrokenAssetMockApiRoutes,
|
||||
} from 'src/ui/mock-network/broken-asset-network';
|
||||
import { assetViewerUtils } from '../timeline/utils';
|
||||
import { setupAssetViewerFixture } from './utils';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
test.describe('broken-asset responsiveness', () => {
|
||||
const fixture = setupAssetViewerFixture(889);
|
||||
let mockStack: MockStack;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
const primaryAssetDto = toAssetResponseDto(fixture.primaryAsset);
|
||||
|
||||
const brokenAssets = [
|
||||
createMockStackAsset(fixture.adminUserId),
|
||||
createMockStackAsset(fixture.adminUserId),
|
||||
createMockStackAsset(fixture.adminUserId),
|
||||
];
|
||||
|
||||
mockStack = createMockStack(primaryAssetDto, brokenAssets);
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await setupBrokenAssetMockApiRoutes(context, mockStack);
|
||||
});
|
||||
|
||||
test('broken asset in stack strip hides icon at small size', async ({ page }) => {
|
||||
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset);
|
||||
|
||||
const stackSlideshow = page.locator('#stack-slideshow');
|
||||
await expect(stackSlideshow).toBeVisible();
|
||||
|
||||
const brokenAssets = stackSlideshow.locator('[data-broken-asset]');
|
||||
await expect(brokenAssets.first()).toBeVisible();
|
||||
await expect(brokenAssets).toHaveCount(mockStack.brokenAssetIds.size);
|
||||
|
||||
for (const brokenAsset of await brokenAssets.all()) {
|
||||
await expect(brokenAsset.locator('svg')).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('broken asset in stack strip uses text-xs class', async ({ page }) => {
|
||||
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
||||
await assetViewerUtils.waitForViewerLoad(page, fixture.primaryAsset);
|
||||
|
||||
const stackSlideshow = page.locator('#stack-slideshow');
|
||||
await expect(stackSlideshow).toBeVisible();
|
||||
|
||||
const brokenAssets = stackSlideshow.locator('[data-broken-asset]');
|
||||
await expect(brokenAssets.first()).toBeVisible();
|
||||
|
||||
for (const brokenAsset of await brokenAssets.all()) {
|
||||
const messageSpan = brokenAsset.locator('span');
|
||||
await expect(messageSpan).toHaveClass(/text-xs/);
|
||||
}
|
||||
});
|
||||
|
||||
test('broken asset in main viewer shows icon and uses text-base', async ({ context, page }) => {
|
||||
await context.route(
|
||||
(url) => url.pathname.includes(`/api/assets/${fixture.primaryAsset.id}/thumbnail`),
|
||||
async (route) => {
|
||||
return route.fulfill({ status: 404 });
|
||||
},
|
||||
);
|
||||
|
||||
await page.goto(`/photos/${fixture.primaryAsset.id}`);
|
||||
await page.waitForSelector('#immich-asset-viewer');
|
||||
|
||||
const viewerBrokenAsset = page.locator('#immich-asset-viewer #broken-asset [data-broken-asset]');
|
||||
await expect(viewerBrokenAsset).toBeVisible();
|
||||
|
||||
await expect(viewerBrokenAsset.locator('svg')).toBeVisible();
|
||||
|
||||
const messageSpan = viewerBrokenAsset.locator('span');
|
||||
await expect(messageSpan).toHaveClass(/text-base/);
|
||||
});
|
||||
});
|
||||
116
e2e/src/ui/specs/asset-viewer/utils.ts
Normal file
116
e2e/src/ui/specs/asset-viewer/utils.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { BrowserContext, Page, test } from '@playwright/test';
|
||||
import {
|
||||
Changes,
|
||||
createDefaultTimelineConfig,
|
||||
generateTimelineData,
|
||||
SeededRandom,
|
||||
selectRandom,
|
||||
TimelineAssetConfig,
|
||||
TimelineData,
|
||||
toAssetResponseDto,
|
||||
} from 'src/ui/generators/timeline';
|
||||
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
||||
import { utils } from 'src/utils';
|
||||
|
||||
export type AssetViewerTestFixture = {
|
||||
adminUserId: string;
|
||||
timelineRestData: TimelineData;
|
||||
assets: TimelineAssetConfig[];
|
||||
testContext: TimelineTestContext;
|
||||
changes: Changes;
|
||||
primaryAsset: TimelineAssetConfig;
|
||||
primaryAssetDto: AssetResponseDto;
|
||||
};
|
||||
|
||||
export function setupAssetViewerFixture(seed: number): AssetViewerTestFixture {
|
||||
const rng = new SeededRandom(seed);
|
||||
const testContext = new TimelineTestContext();
|
||||
|
||||
const fixture: AssetViewerTestFixture = {
|
||||
adminUserId: undefined!,
|
||||
timelineRestData: undefined!,
|
||||
assets: [],
|
||||
testContext,
|
||||
changes: {
|
||||
albumAdditions: [],
|
||||
assetDeletions: [],
|
||||
assetArchivals: [],
|
||||
assetFavorites: [],
|
||||
},
|
||||
primaryAsset: undefined!,
|
||||
primaryAssetDto: undefined!,
|
||||
};
|
||||
|
||||
test.beforeAll(async () => {
|
||||
test.fail(
|
||||
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS !== '1',
|
||||
'This test requires env var: PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1',
|
||||
);
|
||||
utils.initSdk();
|
||||
fixture.adminUserId = faker.string.uuid();
|
||||
testContext.adminId = fixture.adminUserId;
|
||||
fixture.timelineRestData = generateTimelineData({
|
||||
...createDefaultTimelineConfig(),
|
||||
ownerId: fixture.adminUserId,
|
||||
});
|
||||
for (const timeBucket of fixture.timelineRestData.buckets.values()) {
|
||||
fixture.assets.push(...timeBucket);
|
||||
}
|
||||
|
||||
fixture.primaryAsset = selectRandom(
|
||||
fixture.assets.filter((a) => a.isImage),
|
||||
rng,
|
||||
);
|
||||
fixture.primaryAssetDto = toAssetResponseDto(fixture.primaryAsset);
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await setupBaseMockApiRoutes(context, fixture.adminUserId);
|
||||
await setupTimelineMockApiRoutes(context, fixture.timelineRestData, fixture.changes, fixture.testContext);
|
||||
});
|
||||
|
||||
test.afterEach(() => {
|
||||
fixture.testContext.slowBucket = false;
|
||||
fixture.changes.albumAdditions = [];
|
||||
fixture.changes.assetDeletions = [];
|
||||
fixture.changes.assetArchivals = [];
|
||||
fixture.changes.assetFavorites = [];
|
||||
});
|
||||
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export async function ensureDetailPanelVisible(page: Page) {
|
||||
await page.waitForSelector('#immich-asset-viewer');
|
||||
|
||||
const isVisible = await page.locator('#detail-panel').isVisible();
|
||||
if (!isVisible) {
|
||||
await page.keyboard.press('i');
|
||||
await page.waitForSelector('#detail-panel');
|
||||
}
|
||||
}
|
||||
|
||||
export async function enableTagsPreference(context: BrowserContext) {
|
||||
await context.route('**/users/me/preferences', async (route) => {
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
json: {
|
||||
albums: { defaultAssetOrder: 'desc' },
|
||||
folders: { enabled: false, sidebarWeb: false },
|
||||
memories: { enabled: true, duration: 5 },
|
||||
people: { enabled: true, sidebarWeb: false },
|
||||
sharedLinks: { enabled: true, sidebarWeb: false },
|
||||
ratings: { enabled: false },
|
||||
tags: { enabled: true, sidebarWeb: false },
|
||||
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
|
||||
download: { archiveSize: 4_294_967_296, includeEmbeddedVideos: false },
|
||||
purchase: { showSupportBadge: true, hideBuyButtonUntil: '2100-02-12T00:00:00.000Z' },
|
||||
cast: { gCastEnabled: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1074,6 +1074,7 @@
|
||||
"failed_to_update_notification_status": "Failed to update notification status",
|
||||
"incorrect_email_or_password": "Incorrect email or password",
|
||||
"library_folder_already_exists": "This import path already exists.",
|
||||
"page_not_found": "Page not found :/",
|
||||
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
|
||||
"profile_picture_transparent_pixels": "Profile pictures cannot have transparent pixels. Please zoom in and/or move the image.",
|
||||
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
|
||||
|
||||
@@ -23,10 +23,11 @@ import { MediaService } from 'src/services/media.service';
|
||||
import { JobCounts, RawImageInfo } from 'src/types';
|
||||
import { AssetFaceFactory } from 'test/factories/asset-face.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { PersonFactory } from 'test/factories/person.factory';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personStub, personThumbnailStub } from 'test/fixtures/person.stub';
|
||||
import { personThumbnailStub } from 'test/fixtures/person.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { factory, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const fullsizeBuffer = Buffer.from('embedded image data');
|
||||
@@ -50,9 +51,10 @@ describe(MediaService.name, () => {
|
||||
describe('handleQueueGenerateThumbnails', () => {
|
||||
it('should queue all assets', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create({ faceAssetId: newUuid() });
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([asset]));
|
||||
|
||||
mocks.person.getAll.mockReturnValue(makeStream([personStub.newThumbnail]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream([person]));
|
||||
|
||||
await sut.handleQueueGenerateThumbnails({ force: true });
|
||||
|
||||
@@ -68,7 +70,7 @@ describe(MediaService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.PersonGenerateThumbnail,
|
||||
data: { id: personStub.newThumbnail.id },
|
||||
data: { id: person.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -106,8 +108,13 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should queue all people with missing thumbnail path', async () => {
|
||||
const [person1, person2] = [
|
||||
PersonFactory.create({ thumbnailPath: undefined }),
|
||||
PersonFactory.create({ thumbnailPath: undefined }),
|
||||
];
|
||||
|
||||
mocks.assetJob.streamForThumbnailJob.mockReturnValue(makeStream([AssetFactory.create()]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream([personStub.noThumbnail, personStub.noThumbnail]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream([person1, person2]));
|
||||
mocks.person.getRandomFace.mockResolvedValueOnce(AssetFaceFactory.create());
|
||||
|
||||
await sut.handleQueueGenerateThumbnails({ force: false });
|
||||
@@ -120,7 +127,7 @@ describe(MediaService.name, () => {
|
||||
{
|
||||
name: JobName.PersonGenerateThumbnail,
|
||||
data: {
|
||||
id: personStub.newThumbnail.id,
|
||||
id: person1.id,
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -276,17 +283,17 @@ describe(MediaService.name, () => {
|
||||
describe('handleQueueMigration', () => {
|
||||
it('should remove empty directories and queue jobs', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.assetJob.streamForMigrationJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.job.getJobCounts.mockResolvedValue({ active: 1, waiting: 0 } as JobCounts);
|
||||
mocks.person.getAll.mockReturnValue(makeStream([personStub.withName]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream([person]));
|
||||
|
||||
await expect(sut.handleQueueMigration()).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.storage.removeEmptyDirs).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.AssetFileMigration, data: { id: asset.id } }]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{ name: JobName.PersonFileMigration, data: { id: personStub.withName.id } },
|
||||
]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.PersonFileMigration, data: { id: person.id } }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1479,8 +1486,9 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should skip a person without a face asset id', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.noThumbnail);
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
const person = PersonFactory.create({ faceAssetId: null });
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await sut.handleGeneratePersonThumbnail({ id: person.id });
|
||||
expect(mocks.media.generateThumbnail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1490,17 +1498,17 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate a thumbnail', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailMiddle);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
const data = Buffer.from('');
|
||||
const info = { width: 1000, height: 1000 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id);
|
||||
expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(person.id);
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
@@ -1531,21 +1539,21 @@ describe(MediaService.name, () => {
|
||||
},
|
||||
expect.any(String),
|
||||
);
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', thumbnailPath: expect.any(String) });
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, thumbnailPath: expect.any(String) });
|
||||
});
|
||||
|
||||
it('should use preview path if video', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.videoThumbnail);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
const data = Buffer.from('');
|
||||
const info = { width: 1000, height: 1000 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(personStub.primaryPerson.id);
|
||||
expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(person.id);
|
||||
expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String));
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(expect.any(String), {
|
||||
colorspace: Colorspace.P3,
|
||||
@@ -1576,19 +1584,19 @@ describe(MediaService.name, () => {
|
||||
},
|
||||
expect.any(String),
|
||||
);
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', thumbnailPath: expect.any(String) });
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, thumbnailPath: expect.any(String) });
|
||||
});
|
||||
|
||||
it('should generate a thumbnail without going negative', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailStart);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
const data = Buffer.from('');
|
||||
const info = { width: 2160, height: 3840 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailStart.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
@@ -1622,16 +1630,16 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should generate a thumbnail without overflowing', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailEnd);
|
||||
mocks.person.update.mockResolvedValue(personStub.primaryPerson);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
const data = Buffer.from('');
|
||||
const info = { width: 1000, height: 1000 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailEnd.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
@@ -1665,16 +1673,16 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle negative coordinates', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.negativeCoordinate);
|
||||
mocks.person.update.mockResolvedValue(personStub.primaryPerson);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
const data = Buffer.from('');
|
||||
const info = { width: 4624, height: 3080 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.negativeCoordinate.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
@@ -1708,16 +1716,16 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle overflowing coordinate', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.overflowingCoordinate);
|
||||
mocks.person.update.mockResolvedValue(personStub.primaryPerson);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
const data = Buffer.from('');
|
||||
const info = { width: 4624, height: 3080 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.overflowingCoordinate.originalPath, {
|
||||
colorspace: Colorspace.P3,
|
||||
@@ -1751,9 +1759,11 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should use embedded preview if enabled and raw image', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail);
|
||||
mocks.person.update.mockResolvedValue(personStub.primaryPerson);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
const extracted = Buffer.from('');
|
||||
const data = Buffer.from('');
|
||||
@@ -1762,9 +1772,7 @@ describe(MediaService.name, () => {
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 2160, height: 3840, isTransparent: false });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath);
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(extracted, {
|
||||
@@ -1799,21 +1807,23 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should not use embedded preview if enabled and not raw image', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.newThumbnailMiddle);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
const data = Buffer.from('');
|
||||
const info = { width: 2160, height: 3840 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.media.extract).not.toHaveBeenCalled();
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not use embedded preview if enabled and raw image if not exists', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
@@ -1821,9 +1831,7 @@ describe(MediaService.name, () => {
|
||||
const info = { width: 2160, height: 3840 } as OutputInfo;
|
||||
mocks.media.decodeImage.mockResolvedValue({ data, info });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath);
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath, {
|
||||
@@ -1835,6 +1843,8 @@ describe(MediaService.name, () => {
|
||||
});
|
||||
|
||||
it('should not use embedded preview if enabled and raw image if low resolution', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||
mocks.person.getDataForThumbnailGenerationJob.mockResolvedValue(personThumbnailStub.rawEmbeddedThumbnail);
|
||||
mocks.media.generateThumbnail.mockResolvedValue();
|
||||
@@ -1845,9 +1855,7 @@ describe(MediaService.name, () => {
|
||||
mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg });
|
||||
mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false });
|
||||
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id })).resolves.toBe(
|
||||
JobStatus.Success,
|
||||
);
|
||||
await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success);
|
||||
|
||||
expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath);
|
||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath, {
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
import { ImmichTags } from 'src/repositories/metadata.repository';
|
||||
import { firstDateTime, MetadataService } from 'src/services/metadata.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { PersonFactory } from 'test/factories/person.factory';
|
||||
import { probeStub } from 'test/fixtures/media.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { tagStub } from 'test/fixtures/tag.stub';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
@@ -1208,18 +1208,18 @@ describe(MetadataService.name, () => {
|
||||
|
||||
it('should apply metadata face tags creating new people', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: personStub.withName.name }));
|
||||
mockReadTags(makeFaceTags({ Name: person.name }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
mocks.person.createAll.mockResolvedValue([personStub.withName.id]);
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
mocks.person.createAll.mockResolvedValue([person.id]);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true });
|
||||
expect(mocks.person.createAll).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ name: personStub.withName.name }),
|
||||
]);
|
||||
expect(mocks.person.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: person.name })]);
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
@@ -1243,19 +1243,21 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.PersonGenerateThumbnail,
|
||||
data: { id: personStub.withName.id },
|
||||
data: { id: person.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should assign metadata face tags to existing persons', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: personStub.withName.name }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
|
||||
mockReadTags(makeFaceTags({ Name: person.name }));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([{ id: person.id, name: person.name }]);
|
||||
mocks.person.createAll.mockResolvedValue([]);
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, { withHidden: true });
|
||||
@@ -1265,7 +1267,7 @@ describe(MetadataService.name, () => {
|
||||
{
|
||||
id: 'random-uuid',
|
||||
assetId: asset.id,
|
||||
personId: personStub.withName.id,
|
||||
personId: person.id,
|
||||
imageHeight: 100,
|
||||
imageWidth: 1000,
|
||||
boundingBoxX1: 0,
|
||||
@@ -1335,21 +1337,20 @@ describe(MetadataService.name, () => {
|
||||
async ({ orientation, expected }) => {
|
||||
const { imgW, imgH, x1, x2, y1, y2 } = expected;
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(asset);
|
||||
mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||
mockReadTags(makeFaceTags({ Name: personStub.withName.name }, orientation));
|
||||
mockReadTags(makeFaceTags({ Name: person.name }, orientation));
|
||||
mocks.person.getDistinctNames.mockResolvedValue([]);
|
||||
mocks.person.createAll.mockResolvedValue([personStub.withName.id]);
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
mocks.person.createAll.mockResolvedValue([person.id]);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
|
||||
expect(mocks.person.getDistinctNames).toHaveBeenCalledWith(asset.ownerId, {
|
||||
withHidden: true,
|
||||
});
|
||||
expect(mocks.person.createAll).toHaveBeenCalledWith([
|
||||
expect.objectContaining({ name: personStub.withName.name }),
|
||||
]);
|
||||
expect(mocks.person.createAll).toHaveBeenCalledWith([expect.objectContaining({ name: person.name })]);
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
@@ -1373,7 +1374,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.PersonGenerateThumbnail,
|
||||
data: { id: personStub.withName.id },
|
||||
data: { id: person.id },
|
||||
},
|
||||
]);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||
import { mapFaces, mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
|
||||
import { mapFaces, mapPerson } from 'src/dtos/person.dto';
|
||||
import { AssetFileType, CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum';
|
||||
import { FaceSearchResult } from 'src/repositories/search.repository';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
@@ -11,25 +11,11 @@ import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { PersonFactory } from 'test/factories/person.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { getAsDetectedFace, getForFacialRecognitionJob } from 'test/mappers';
|
||||
import { newDate, newUuid } from 'test/small.factory';
|
||||
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
const responseDto: PersonResponseDto = {
|
||||
id: 'person-1',
|
||||
name: 'Person 1',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
isHidden: false,
|
||||
updatedAt: expect.any(Date),
|
||||
isFavorite: false,
|
||||
color: expect.any(String),
|
||||
};
|
||||
|
||||
const statistics = { assets: 3 };
|
||||
|
||||
describe(PersonService.name, () => {
|
||||
let sut: PersonService;
|
||||
let mocks: ServiceMocks;
|
||||
@@ -44,60 +30,54 @@ describe(PersonService.name, () => {
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should get all hidden and visible people with thumbnails', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [person, hiddenPerson] = [PersonFactory.create(), PersonFactory.create({ isHidden: true })];
|
||||
|
||||
mocks.person.getAllForUser.mockResolvedValue({
|
||||
items: [personStub.withName, personStub.hidden],
|
||||
items: [person, hiddenPerson],
|
||||
hasNextPage: false,
|
||||
});
|
||||
mocks.person.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 });
|
||||
await expect(sut.getAll(authStub.admin, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({
|
||||
await expect(sut.getAll(auth, { withHidden: true, page: 1, size: 10 })).resolves.toEqual({
|
||||
hasNextPage: false,
|
||||
total: 2,
|
||||
hidden: 1,
|
||||
people: [
|
||||
responseDto,
|
||||
{
|
||||
id: 'person-1',
|
||||
name: '',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
expect.objectContaining({ id: person.id, isHidden: false }),
|
||||
expect.objectContaining({
|
||||
id: hiddenPerson.id,
|
||||
isHidden: true,
|
||||
isFavorite: false,
|
||||
updatedAt: expect.any(Date),
|
||||
color: expect.any(String),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, {
|
||||
expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, {
|
||||
minimumFaceCount: 3,
|
||||
withHidden: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should get all visible people and favorites should be first in the array', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const [isFavorite, person] = [PersonFactory.create({ isFavorite: true }), PersonFactory.create()];
|
||||
|
||||
mocks.person.getAllForUser.mockResolvedValue({
|
||||
items: [personStub.isFavorite, personStub.withName],
|
||||
items: [isFavorite, person],
|
||||
hasNextPage: false,
|
||||
});
|
||||
mocks.person.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 });
|
||||
await expect(sut.getAll(authStub.admin, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({
|
||||
await expect(sut.getAll(auth, { withHidden: false, page: 1, size: 10 })).resolves.toEqual({
|
||||
hasNextPage: false,
|
||||
total: 2,
|
||||
hidden: 1,
|
||||
people: [
|
||||
{
|
||||
id: 'person-4',
|
||||
name: personStub.isFavorite.name,
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
isHidden: false,
|
||||
expect.objectContaining({
|
||||
id: isFavorite.id,
|
||||
isFavorite: true,
|
||||
updatedAt: expect.any(Date),
|
||||
color: personStub.isFavorite.color,
|
||||
},
|
||||
responseDto,
|
||||
}),
|
||||
expect.objectContaining({ id: person.id, isFavorite: false }),
|
||||
],
|
||||
});
|
||||
expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, authStub.admin.user.id, {
|
||||
expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, {
|
||||
minimumFaceCount: 3,
|
||||
withHidden: false,
|
||||
});
|
||||
@@ -106,71 +86,89 @@ describe(PersonService.name, () => {
|
||||
|
||||
describe('getById', () => {
|
||||
it('should require person.read permission', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.withName);
|
||||
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await expect(sut.getById(auth, person.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should throw a bad request when person is not found', async () => {
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['unknown']));
|
||||
await expect(sut.getById(auth, 'unknown')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['unknown']));
|
||||
});
|
||||
|
||||
it('should get a person by id', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.withName);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto);
|
||||
expect(mocks.person.getById).toHaveBeenCalledWith('person-1');
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
await expect(sut.getById(auth, person.id)).resolves.toEqual(expect.objectContaining({ id: person.id }));
|
||||
expect(mocks.person.getById).toHaveBeenCalledWith(person.id);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThumbnail', () => {
|
||||
it('should require person.read permission', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.noName);
|
||||
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await expect(sut.getThumbnail(auth, person.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.storage.createReadStream).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should throw an error when personId is invalid', async () => {
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
|
||||
const auth = AuthFactory.create();
|
||||
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['unknown']));
|
||||
await expect(sut.getThumbnail(auth, 'unknown')).rejects.toBeInstanceOf(NotFoundException);
|
||||
expect(mocks.storage.createReadStream).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['unknown']));
|
||||
});
|
||||
|
||||
it('should throw an error when person has no thumbnail', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.noThumbnail);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException);
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create({ thumbnailPath: '' });
|
||||
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
await expect(sut.getThumbnail(auth, person.id)).rejects.toBeInstanceOf(NotFoundException);
|
||||
expect(mocks.storage.createReadStream).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should serve the thumbnail', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.noName);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getThumbnail(authStub.admin, 'person-1')).resolves.toEqual(
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
await expect(sut.getThumbnail(auth, person.id)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: '/path/to/thumbnail.jpg',
|
||||
path: person.thumbnailPath,
|
||||
contentType: 'image/jpeg',
|
||||
cacheControl: CacheControl.PrivateWithoutCache,
|
||||
}),
|
||||
);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should require person.write permission', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.noName);
|
||||
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await expect(sut.update(auth, person.id, { name: 'Person 1' })).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should throw an error when personId is invalid', async () => {
|
||||
@@ -183,86 +181,108 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it("should update a person's name", async () => {
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create({ name: 'Person 1' });
|
||||
|
||||
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' });
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
await expect(sut.update(auth, person.id, { name: 'Person 1' })).resolves.toEqual(
|
||||
expect.objectContaining({ id: person.id, name: 'Person 1' }),
|
||||
);
|
||||
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, name: 'Person 1' });
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it("should update a person's date of birth", async () => {
|
||||
mocks.person.update.mockResolvedValue(personStub.withBirthDate);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create({ birthDate: new Date('1976-06-30') });
|
||||
|
||||
await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({
|
||||
id: 'person-1',
|
||||
name: 'Person 1',
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
await expect(sut.update(auth, person.id, { birthDate: new Date('1976-06-30') })).resolves.toEqual({
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
birthDate: '1976-06-30',
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
updatedAt: expect.any(Date),
|
||||
color: expect.any(String),
|
||||
});
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') });
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: new Date('1976-06-30') });
|
||||
expect(mocks.job.queue).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should update a person visibility', async () => {
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create({ isHidden: true });
|
||||
|
||||
await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false });
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
await expect(sut.update(auth, person.id, { isHidden: true })).resolves.toEqual(
|
||||
expect.objectContaining({ isHidden: true }),
|
||||
);
|
||||
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, isHidden: true });
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should update a person favorite status', async () => {
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create({ isFavorite: true });
|
||||
|
||||
await expect(sut.update(authStub.admin, 'person-1', { isFavorite: true })).resolves.toEqual(responseDto);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', isFavorite: true });
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
await expect(sut.update(auth, person.id, { isFavorite: true })).resolves.toEqual(
|
||||
expect.objectContaining({ isFavorite: true }),
|
||||
);
|
||||
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, isFavorite: true });
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it("should update a person's thumbnailPath", async () => {
|
||||
const face = AssetFaceFactory.create();
|
||||
const auth = AuthFactory.create();
|
||||
mocks.person.update.mockResolvedValue(personStub.withName);
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
mocks.person.getForFeatureFaceUpdate.mockResolvedValue(face);
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([face.assetId]));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
await expect(sut.update(auth, 'person-1', { featureFaceAssetId: face.assetId })).resolves.toEqual(responseDto);
|
||||
await expect(sut.update(auth, person.id, { featureFaceAssetId: face.assetId })).resolves.toEqual(
|
||||
expect.objectContaining({ id: person.id }),
|
||||
);
|
||||
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: face.id });
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, faceAssetId: face.id });
|
||||
expect(mocks.person.getForFeatureFaceUpdate).toHaveBeenCalledWith({
|
||||
assetId: face.assetId,
|
||||
personId: 'person-1',
|
||||
personId: person.id,
|
||||
});
|
||||
expect(mocks.job.queue).toHaveBeenCalledWith({
|
||||
name: JobName.PersonGenerateThumbnail,
|
||||
data: { id: 'person-1' },
|
||||
data: { id: person.id },
|
||||
});
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should throw an error when the face feature assetId is invalid', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.withName);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
await expect(sut.update(authStub.admin, 'person-1', { featureFaceAssetId: '-1' })).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
await expect(sut.update(auth, person.id, { featureFaceAssetId: '-1' })).rejects.toThrow(BadRequestException);
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -283,36 +303,39 @@ describe(PersonService.name, () => {
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
|
||||
await expect(
|
||||
sut.reassignFaces(authStub.admin, personStub.noName.id, {
|
||||
sut.reassignFaces(AuthFactory.create(), 'person-id', {
|
||||
data: [{ personId: 'asset-face-1', assetId: '' }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.job.queue).not.toHaveBeenCalledWith();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should reassign a face', async () => {
|
||||
const face = AssetFaceFactory.create();
|
||||
const auth = AuthFactory.create();
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.withName.id]));
|
||||
mocks.person.getById.mockResolvedValue(personStub.noName);
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id]));
|
||||
mocks.person.getFacesByIds.mockResolvedValue([face]);
|
||||
mocks.person.reassignFace.mockResolvedValue(1);
|
||||
mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create());
|
||||
mocks.person.refreshFaces.mockResolvedValue();
|
||||
mocks.person.reassignFace.mockResolvedValue(5);
|
||||
mocks.person.update.mockResolvedValue(personStub.noName);
|
||||
mocks.person.update.mockResolvedValue(person);
|
||||
|
||||
await expect(
|
||||
sut.reassignFaces(auth, personStub.noName.id, {
|
||||
data: [{ personId: personStub.withName.id, assetId: face.assetId }],
|
||||
sut.reassignFaces(auth, person.id, {
|
||||
data: [{ personId: person.id, assetId: face.assetId }],
|
||||
}),
|
||||
).resolves.toBeDefined();
|
||||
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.PersonGenerateThumbnail,
|
||||
data: { id: personStub.newThumbnail.id },
|
||||
data: { id: person.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -320,7 +343,7 @@ describe(PersonService.name, () => {
|
||||
|
||||
describe('handlePersonMigration', () => {
|
||||
it('should not move person files', async () => {
|
||||
await expect(sut.handlePersonMigration(personStub.noName)).resolves.toBe(JobStatus.Failed);
|
||||
await expect(sut.handlePersonMigration(PersonFactory.create())).resolves.toBe(JobStatus.Failed);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -347,12 +370,14 @@ describe(PersonService.name, () => {
|
||||
|
||||
describe('createNewFeaturePhoto', () => {
|
||||
it('should change person feature photo', async () => {
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create());
|
||||
await sut.createNewFeaturePhoto([personStub.newThumbnail.id]);
|
||||
await sut.createNewFeaturePhoto([person.id]);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
name: JobName.PersonGenerateThumbnail,
|
||||
data: { id: personStub.newThumbnail.id },
|
||||
data: { id: person.id },
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -361,23 +386,22 @@ describe(PersonService.name, () => {
|
||||
describe('reassignFacesById', () => {
|
||||
it('should create a new person', async () => {
|
||||
const face = AssetFaceFactory.create();
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id]));
|
||||
mocks.person.getFaceById.mockResolvedValue(face);
|
||||
mocks.person.reassignFace.mockResolvedValue(1);
|
||||
mocks.person.getById.mockResolvedValue(personStub.noName);
|
||||
await expect(sut.reassignFacesById(AuthFactory.create(), personStub.noName.id, { id: face.id })).resolves.toEqual(
|
||||
{
|
||||
birthDate: personStub.noName.birthDate,
|
||||
isHidden: personStub.noName.isHidden,
|
||||
isFavorite: personStub.noName.isFavorite,
|
||||
id: personStub.noName.id,
|
||||
name: personStub.noName.name,
|
||||
thumbnailPath: personStub.noName.thumbnailPath,
|
||||
updatedAt: expect.any(Date),
|
||||
color: personStub.noName.color,
|
||||
},
|
||||
);
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await expect(sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id })).resolves.toEqual({
|
||||
birthDate: person.birthDate,
|
||||
isHidden: person.isHidden,
|
||||
isFavorite: person.isFavorite,
|
||||
id: person.id,
|
||||
name: person.name,
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
|
||||
expect(mocks.job.queue).not.toHaveBeenCalledWith();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalledWith();
|
||||
@@ -385,12 +409,14 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should fail if user has not the correct permissions on the asset', async () => {
|
||||
const face = AssetFaceFactory.create();
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
mocks.person.getFaceById.mockResolvedValue(face);
|
||||
mocks.person.reassignFace.mockResolvedValue(1);
|
||||
mocks.person.getById.mockResolvedValue(personStub.noName);
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await expect(
|
||||
sut.reassignFacesById(AuthFactory.create(), personStub.noName.id, {
|
||||
sut.reassignFacesById(AuthFactory.create(), person.id, {
|
||||
id: face.id,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
@@ -402,22 +428,25 @@ describe(PersonService.name, () => {
|
||||
|
||||
describe('createPerson', () => {
|
||||
it('should create a new person', async () => {
|
||||
mocks.person.create.mockResolvedValue(personStub.primaryPerson);
|
||||
const auth = AuthFactory.create();
|
||||
|
||||
await expect(sut.create(authStub.admin, {})).resolves.toBeDefined();
|
||||
mocks.person.create.mockResolvedValue(PersonFactory.create());
|
||||
await expect(sut.create(auth, {})).resolves.toBeDefined();
|
||||
|
||||
expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id });
|
||||
expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: auth.user.id });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonCleanup', () => {
|
||||
it('should delete people without faces', async () => {
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.noName]);
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([person]);
|
||||
|
||||
await sut.handlePersonCleanup();
|
||||
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.noName.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.noName.thumbnailPath);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([person.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -449,15 +478,17 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should queue all assets', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.withName]);
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([person]);
|
||||
|
||||
await sut.handleQueueDetectFaces({ force: true });
|
||||
|
||||
expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MachineLearning });
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.withName.id]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([person.id]);
|
||||
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true });
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.withName.thumbnailPath);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath);
|
||||
expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true);
|
||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||
{
|
||||
@@ -490,10 +521,12 @@ describe(PersonService.name, () => {
|
||||
it('should delete existing people and faces if forced', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
const face = AssetFaceFactory.from().person().build();
|
||||
mocks.person.getAll.mockReturnValue(makeStream([face.person!, personStub.randomPerson]));
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getAll.mockReturnValue(makeStream([face.person!, person]));
|
||||
mocks.person.getAllFaces.mockReturnValue(makeStream([face]));
|
||||
mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset]));
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([person]);
|
||||
mocks.person.deleteFaces.mockResolvedValue();
|
||||
|
||||
await sut.handleQueueDetectFaces({ force: true });
|
||||
@@ -505,8 +538,8 @@ describe(PersonService.name, () => {
|
||||
data: { id: asset.id },
|
||||
},
|
||||
]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([person.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath);
|
||||
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true });
|
||||
});
|
||||
});
|
||||
@@ -661,6 +694,8 @@ describe(PersonService.name, () => {
|
||||
|
||||
it('should delete existing people if forced', async () => {
|
||||
const face = AssetFaceFactory.from().person().build();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.job.getJobCounts.mockResolvedValue({
|
||||
active: 1,
|
||||
waiting: 0,
|
||||
@@ -669,9 +704,9 @@ describe(PersonService.name, () => {
|
||||
failed: 0,
|
||||
delayed: 0,
|
||||
});
|
||||
mocks.person.getAll.mockReturnValue(makeStream([face.person!, personStub.randomPerson]));
|
||||
mocks.person.getAll.mockReturnValue(makeStream([face.person!, person]));
|
||||
mocks.person.getAllFaces.mockReturnValue(makeStream([face]));
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
|
||||
mocks.person.getAllWithoutFaces.mockResolvedValue([person]);
|
||||
mocks.person.unassignFaces.mockResolvedValue();
|
||||
|
||||
await sut.handleQueueRecognizeFaces({ force: true });
|
||||
@@ -684,8 +719,8 @@ describe(PersonService.name, () => {
|
||||
data: { id: face.id, deferred: false },
|
||||
},
|
||||
]);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([personStub.randomPerson.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
||||
expect(mocks.person.delete).toHaveBeenCalledWith([person.id]);
|
||||
expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath);
|
||||
expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false });
|
||||
});
|
||||
});
|
||||
@@ -1059,59 +1094,71 @@ describe(PersonService.name, () => {
|
||||
|
||||
describe('mergePerson', () => {
|
||||
it('should require person.write and person.merge permission', async () => {
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
const auth = AuthFactory.create();
|
||||
const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()];
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf(
|
||||
mocks.person.getById.mockResolvedValueOnce(person);
|
||||
mocks.person.getById.mockResolvedValueOnce(mergePerson);
|
||||
|
||||
await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(mocks.person.reassignFaces).not.toHaveBeenCalled();
|
||||
|
||||
expect(mocks.person.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should merge two people without smart merge', async () => {
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
const auth = AuthFactory.create();
|
||||
const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()];
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
|
||||
{ id: 'person-2', success: true },
|
||||
mocks.person.getById.mockResolvedValueOnce(person);
|
||||
mocks.person.getById.mockResolvedValueOnce(mergePerson);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id]));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id]));
|
||||
|
||||
await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([
|
||||
{ id: mergePerson.id, success: true },
|
||||
]);
|
||||
|
||||
expect(mocks.person.reassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.primaryPerson.id,
|
||||
oldPersonId: personStub.mergePerson.id,
|
||||
newPersonId: person.id,
|
||||
oldPersonId: mergePerson.id,
|
||||
});
|
||||
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should merge two people with smart merge', async () => {
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.randomPerson);
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
mocks.person.update.mockResolvedValue({ ...personStub.randomPerson, name: personStub.primaryPerson.name });
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3']));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const [person, mergePerson] = [
|
||||
PersonFactory.create({ name: undefined }),
|
||||
PersonFactory.create({ name: 'Merge person' }),
|
||||
];
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-3', { ids: ['person-1'] })).resolves.toEqual([
|
||||
{ id: 'person-1', success: true },
|
||||
mocks.person.getById.mockResolvedValueOnce(person);
|
||||
mocks.person.getById.mockResolvedValueOnce(mergePerson);
|
||||
mocks.person.update.mockResolvedValue({ ...person, name: mergePerson.name });
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id]));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id]));
|
||||
|
||||
await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([
|
||||
{ id: mergePerson.id, success: true },
|
||||
]);
|
||||
|
||||
expect(mocks.person.reassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.randomPerson.id,
|
||||
oldPersonId: personStub.primaryPerson.id,
|
||||
newPersonId: person.id,
|
||||
oldPersonId: mergePerson.id,
|
||||
});
|
||||
|
||||
expect(mocks.person.update).toHaveBeenCalledWith({
|
||||
id: personStub.randomPerson.id,
|
||||
name: personStub.primaryPerson.name,
|
||||
id: person.id,
|
||||
name: mergePerson.name,
|
||||
});
|
||||
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should throw an error when the primary person is not found', async () => {
|
||||
@@ -1126,48 +1173,60 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
|
||||
it('should handle invalid merge ids', async () => {
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
|
||||
{ id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND },
|
||||
mocks.person.getById.mockResolvedValueOnce(person);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id]));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['unknown']));
|
||||
|
||||
await expect(sut.mergePerson(auth, person.id, { ids: ['unknown'] })).resolves.toEqual([
|
||||
{ id: 'unknown', success: false, error: BulkIdErrorReason.NOT_FOUND },
|
||||
]);
|
||||
|
||||
expect(mocks.person.reassignFaces).not.toHaveBeenCalled();
|
||||
expect(mocks.person.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should handle an error reassigning faces', async () => {
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
mocks.person.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
mocks.person.reassignFaces.mockRejectedValue(new Error('update failed'));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
const auth = AuthFactory.create();
|
||||
const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()];
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
|
||||
{ id: 'person-2', success: false, error: BulkIdErrorReason.UNKNOWN },
|
||||
mocks.person.getById.mockResolvedValueOnce(person);
|
||||
mocks.person.getById.mockResolvedValueOnce(mergePerson);
|
||||
mocks.person.reassignFaces.mockRejectedValue(new Error('update failed'));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id]));
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id]));
|
||||
|
||||
await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([
|
||||
{ id: mergePerson.id, success: false, error: BulkIdErrorReason.UNKNOWN },
|
||||
]);
|
||||
|
||||
expect(mocks.person.delete).not.toHaveBeenCalled();
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStatistics', () => {
|
||||
it('should get correct number of person', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
mocks.person.getStatistics.mockResolvedValue(statistics);
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 });
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
mocks.person.getStatistics.mockResolvedValue({ assets: 3 });
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
await expect(sut.getStatistics(auth, person.id)).resolves.toEqual({ assets: 3 });
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
|
||||
it('should require person.read permission', async () => {
|
||||
mocks.person.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
const auth = AuthFactory.create();
|
||||
const person = PersonFactory.create();
|
||||
|
||||
mocks.person.getById.mockResolvedValue(person);
|
||||
await expect(sut.getStatistics(auth, person.id)).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id]));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -595,7 +595,7 @@ export class PersonService extends BaseService {
|
||||
update.birthDate = mergePerson.birthDate;
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
if (Object.keys(update).length > 1) {
|
||||
primaryPerson = await this.personRepository.update(update);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { SearchService } from 'src/services/search.service';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { AuthFactory } from 'test/factories/auth.factory';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
import { beforeEach, vitest } from 'vitest';
|
||||
|
||||
@@ -26,17 +25,18 @@ describe(SearchService.name, () => {
|
||||
|
||||
describe('searchPerson', () => {
|
||||
it('should pass options to search', async () => {
|
||||
const { name } = personStub.withName;
|
||||
const auth = AuthFactory.create();
|
||||
const name = 'foo';
|
||||
|
||||
mocks.person.getByName.mockResolvedValue([]);
|
||||
|
||||
await sut.searchPerson(authStub.user1, { name, withHidden: false });
|
||||
await sut.searchPerson(auth, { name, withHidden: false });
|
||||
|
||||
expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false });
|
||||
expect(mocks.person.getByName).toHaveBeenCalledWith(auth.user.id, name, { withHidden: false });
|
||||
|
||||
await sut.searchPerson(authStub.user1, { name, withHidden: true });
|
||||
await sut.searchPerson(auth, { name, withHidden: true });
|
||||
|
||||
expect(mocks.person.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: true });
|
||||
expect(mocks.person.getByName).toHaveBeenCalledWith(auth.user.id, name, { withHidden: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
165
server/test/fixtures/person.stub.ts
vendored
165
server/test/fixtures/person.stub.ts
vendored
@@ -2,171 +2,6 @@ import { AssetFileType, AssetType } from 'src/enum';
|
||||
import { AssetFileFactory } from 'test/factories/asset-file.factory';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
|
||||
const updateId = '0d1173e3-4d80-4d76-b41e-57d56de21125';
|
||||
|
||||
export const personStub = {
|
||||
noName: Object.freeze({
|
||||
id: 'person-1',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: '',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
hidden: Object.freeze({
|
||||
id: 'person-1',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: '',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: true,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
withName: Object.freeze({
|
||||
id: 'person-1',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: 'Person 1',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
faces: [],
|
||||
faceAssetId: 'assetFaceId',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
withBirthDate: Object.freeze({
|
||||
id: 'person-1',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: 'Person 1',
|
||||
birthDate: new Date('1976-06-30'),
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
noThumbnail: Object.freeze({
|
||||
id: 'person-1',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: '',
|
||||
birthDate: null,
|
||||
thumbnailPath: '',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
newThumbnail: Object.freeze({
|
||||
id: 'person-1',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: '',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/new/path/to/thumbnail.jpg',
|
||||
faces: [],
|
||||
faceAssetId: 'asset-id',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
primaryPerson: Object.freeze({
|
||||
id: 'person-1',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: 'Person 1',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
mergePerson: Object.freeze({
|
||||
id: 'person-2',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: 'Person 2',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
randomPerson: Object.freeze({
|
||||
id: 'person-3',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: '',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: false,
|
||||
color: 'red',
|
||||
}),
|
||||
isFavorite: Object.freeze({
|
||||
id: 'person-4',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
updatedAt: new Date('2021-01-01'),
|
||||
updateId,
|
||||
ownerId: userStub.admin.id,
|
||||
name: 'Person 1',
|
||||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
faces: [],
|
||||
faceAssetId: 'assetFaceId',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
isFavorite: true,
|
||||
color: 'red',
|
||||
}),
|
||||
};
|
||||
|
||||
export const personThumbnailStub = {
|
||||
newThumbnailStart: Object.freeze({
|
||||
ownerId: userStub.admin.id,
|
||||
|
||||
@@ -15,15 +15,18 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-broken-asset
|
||||
class={[
|
||||
'flex flex-col overflow-hidden max-h-full max-w-full justify-center items-center bg-gray-100/40 dark:bg-gray-700/40 dark:text-gray-100 p-4',
|
||||
'@container flex flex-col overflow-hidden max-h-full max-w-full justify-center items-center bg-gray-100/40 dark:bg-gray-700/40 dark:text-gray-100 p-4',
|
||||
className,
|
||||
]}
|
||||
style:width
|
||||
style:height
|
||||
>
|
||||
<Icon icon={mdiImageBrokenVariant} size="7em" class="max-w-full" />
|
||||
<div class="hidden @min-[75px]:block">
|
||||
<Icon icon={mdiImageBrokenVariant} size="7em" class="max-w-full min-w-6 min-h-6" />
|
||||
</div>
|
||||
{#if !hideMessage}
|
||||
<span class="text-center">{$t('error_loading_image')}</span>
|
||||
<span class="text-center text-xs @min-[100px]:text-sm @min-[150px]:text-base">{$t('error_loading_image')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Oops! Error - Immich</title>
|
||||
<title>{$t('error')} - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="flex flex-col px-4 h-dvh w-dvw place-content-center place-items-center">
|
||||
<h1 class="py-10 text-4xl text-primary">Page not found :/</h1>
|
||||
<h1 class="py-10 text-4xl text-primary">{$t('errors.page_not_found')}</h1>
|
||||
{#if page.error?.message}
|
||||
<h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user