mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
chore: metadata extraction date test (#26102)
This commit is contained in:
parent
7fa6f617f5
commit
71fe9192fd
1 changed files with 93 additions and 46 deletions
|
|
@ -1,24 +1,21 @@
|
||||||
|
import { Kysely } from 'kysely';
|
||||||
import { Stats } from 'node:fs';
|
import { Stats } from 'node:fs';
|
||||||
import { writeFile } from 'node:fs/promises';
|
import { writeFile } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
|
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
|
import { TagRepository } from 'src/repositories/tag.repository';
|
||||||
|
import { DB } from 'src/schema';
|
||||||
import { MetadataService } from 'src/services/metadata.service';
|
import { MetadataService } from 'src/services/metadata.service';
|
||||||
import { automock, newRandomImage, newTestService, ServiceMocks } from 'test/utils';
|
import { newMediumService } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB, newRandomImage } from 'test/utils';
|
||||||
const metadataRepository = new MetadataRepository(
|
|
||||||
// eslint-disable-next-line no-sparse-arrays
|
|
||||||
automock(LoggingRepository, { args: [, { getEnv: () => ({}) }], strict: false }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const createTestFile = async (exifData: Record<string, any>) => {
|
|
||||||
const data = newRandomImage();
|
|
||||||
const filePath = join(tmpdir(), 'test.png');
|
|
||||||
await writeFile(filePath, data);
|
|
||||||
await metadataRepository.writeTags(filePath, exifData);
|
|
||||||
return { filePath };
|
|
||||||
};
|
|
||||||
|
|
||||||
type TimeZoneTest = {
|
type TimeZoneTest = {
|
||||||
description: string;
|
description: string;
|
||||||
|
|
@ -31,24 +28,52 @@ type TimeZoneTest = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
describe(MetadataService.name, () => {
|
let defaultDatabase: Kysely<DB>;
|
||||||
let sut: MetadataService;
|
|
||||||
let mocks: ServiceMocks;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
const setup = (db?: Kysely<DB>) => {
|
||||||
({ sut, mocks } = newTestService(MetadataService, { metadata: metadataRepository }));
|
const { sut, ctx } = newMediumService(MetadataService, {
|
||||||
|
database: db || defaultDatabase,
|
||||||
|
real: [
|
||||||
|
AssetRepository,
|
||||||
|
AssetJobRepository,
|
||||||
|
ConfigRepository,
|
||||||
|
MetadataRepository,
|
||||||
|
SystemMetadataRepository,
|
||||||
|
TagRepository,
|
||||||
|
],
|
||||||
|
mock: [EventRepository, StorageRepository, LoggingRepository],
|
||||||
|
});
|
||||||
|
|
||||||
mocks.storage.stat.mockResolvedValue({
|
ctx.getMock(StorageRepository).stat.mockResolvedValue({
|
||||||
size: 123_456,
|
size: 123_456,
|
||||||
mtime: new Date(654_321),
|
mtime: new Date(654_321),
|
||||||
mtimeMs: 654_321,
|
mtimeMs: 654_321,
|
||||||
birthtimeMs: 654_322,
|
birthtimeMs: 654_322,
|
||||||
} as Stats);
|
} as Stats);
|
||||||
|
|
||||||
delete process.env.TZ;
|
return { sut, ctx };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTestFile = async (exifData: Record<string, any>) => {
|
||||||
|
const { ctx } = setup();
|
||||||
|
const data = newRandomImage();
|
||||||
|
const filePath = join(tmpdir(), 'test.png');
|
||||||
|
await writeFile(filePath, data);
|
||||||
|
await ctx.get(MetadataRepository).writeTags(filePath, exifData);
|
||||||
|
return { filePath };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(MetadataService.name, () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
|
const { sut } = setup();
|
||||||
expect(sut).toBeDefined();
|
expect(sut).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -79,30 +104,52 @@ describe(MetadataService.name, () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(timeZoneTests)('$description', async ({ exifData, serverTimeZone, expected }) => {
|
it.each(timeZoneTests)('$description', async ({ exifData, serverTimeZone, expected }) => {
|
||||||
process.env.TZ = serverTimeZone ?? undefined;
|
vi.stubEnv('TZ', serverTimeZone);
|
||||||
|
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
const { filePath } = await createTestFile(exifData);
|
const { filePath } = await createTestFile(exifData);
|
||||||
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
const { user } = await ctx.newUser();
|
||||||
id: 'asset-1',
|
const { asset } = await ctx.newAsset({ originalPath: filePath, ownerId: user.id });
|
||||||
originalPath: filePath,
|
await ctx.newExif({ assetId: asset.id, description: '' });
|
||||||
files: [],
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: 'asset-1' });
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
expect(mocks.asset.upsertExif).toHaveBeenCalledWith(
|
await expect(
|
||||||
expect.objectContaining({
|
ctx.database
|
||||||
|
.selectFrom('asset_exif')
|
||||||
|
.select(['dateTimeOriginal', 'timeZone', 'lockedProperties'])
|
||||||
|
.where('assetId', '=', asset.id)
|
||||||
|
.executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({
|
||||||
dateTimeOriginal: new Date(expected.dateTimeOriginal),
|
dateTimeOriginal: new Date(expected.dateTimeOriginal),
|
||||||
timeZone: expected.timeZone,
|
timeZone: expected.timeZone,
|
||||||
}),
|
lockedProperties: null,
|
||||||
{ lockedPropertiesBehavior: 'skip' },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({ localDateTime: new Date(expected.localDateTime) }),
|
||||||
localDateTime: new Date(expected.localDateTime),
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle dates far in the future', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||||
|
const { filePath } = await createTestFile({ CreateDate: '42603:05:04 04:12:48' });
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ originalPath: filePath, ownerId: user.id });
|
||||||
|
await ctx.newExif({ assetId: asset.id, description: '' });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: asset.id });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database
|
||||||
|
.selectFrom('asset_exif')
|
||||||
|
.where('assetId', '=', asset.id)
|
||||||
|
.select('dateTimeOriginal')
|
||||||
|
.executeTakeFirstOrThrow(),
|
||||||
|
// note that this date is technically wrong. it does not throw though and should get the user's attention either way.
|
||||||
|
).resolves.toEqual({ dateTimeOriginal: new Date('4260-03-05T04:04:12.000Z') });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue