mirror of
https://github.com/immich-app/immich.git
synced 2026-02-11 19:38:54 +03:00
Merge remote-tracking branch 'origin/main' into refactor/database-backup-repository
This commit is contained in:
@@ -6,6 +6,7 @@ import { Stats } from 'node:fs';
|
||||
import { Writable } from 'node:stream';
|
||||
import { AssetFace } from 'src/database';
|
||||
import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto';
|
||||
import { AssetEditActionListDto } from 'src/dtos/editing.dto';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
AssetType,
|
||||
@@ -280,6 +281,11 @@ export class MediumTestContext<S extends BaseService = BaseService> {
|
||||
const result = await this.get(TagRepository).upsertAssetIds(tagsAssets);
|
||||
return { tagsAssets, result };
|
||||
}
|
||||
|
||||
async newEdits(assetId: string, dto: AssetEditActionListDto) {
|
||||
const edits = await this.get(AssetEditRepository).replaceAll(assetId, dto.edits);
|
||||
return { edits };
|
||||
}
|
||||
}
|
||||
|
||||
export class SyncTestContext extends MediumTestContext<SyncService> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { AssetFileType, AssetMetadataKey, JobName, SharedLinkType } from 'src/enum';
|
||||
import { AssetFileType, AssetMetadataKey, AssetStatus, JobName, SharedLinkType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||
@@ -246,6 +246,66 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should delete a stacked primary asset (2 assets)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { stack, result } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id]);
|
||||
|
||||
const stackRepo = ctx.get(StackRepository);
|
||||
|
||||
expect(result).toMatchObject({ primaryAssetId: asset1.id });
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset1.id, deleteOnDisk: true });
|
||||
|
||||
// stack is deleted as well
|
||||
await expect(stackRepo.getById(stack.id)).resolves.toBe(undefined);
|
||||
});
|
||||
|
||||
it('should delete a stacked primary asset (3 assets)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { asset: asset3 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { stack, result } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id, asset3.id]);
|
||||
|
||||
expect(result).toMatchObject({ primaryAssetId: asset1.id });
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset1.id, deleteOnDisk: true });
|
||||
|
||||
// new primary asset is picked
|
||||
await expect(ctx.get(StackRepository).getById(stack.id)).resolves.toMatchObject({ primaryAssetId: asset2.id });
|
||||
});
|
||||
|
||||
it('should delete a stacked primary asset (3 trashed assets)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const { asset: asset1 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { asset: asset2 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { asset: asset3 } = await ctx.newAsset({ ownerId: user.id });
|
||||
const { stack, result } = await ctx.newStack({ ownerId: user.id }, [asset1.id, asset2.id, asset3.id]);
|
||||
|
||||
await ctx.get(AssetRepository).updateAll([asset1.id, asset2.id, asset3.id], {
|
||||
deletedAt: new Date(),
|
||||
status: AssetStatus.Deleted,
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ primaryAssetId: asset1.id });
|
||||
|
||||
await sut.handleAssetDeletion({ id: asset1.id, deleteOnDisk: true });
|
||||
|
||||
// stack is deleted as well
|
||||
await expect(ctx.get(StackRepository).getById(stack.id)).resolves.toBe(undefined);
|
||||
});
|
||||
|
||||
it('should not delete offline assets', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(EventRepository).emit.mockResolvedValue();
|
||||
@@ -396,6 +456,47 @@ describe(AssetService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should relatively update an assets with timezone', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00', timeZone: 'UTC+5' });
|
||||
|
||||
await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -1441 });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2023-11-18T18:10:00+00:00',
|
||||
timeZone: 'UTC+5',
|
||||
lockedProperties: ['timeZone', 'dateTimeOriginal'],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should relatively update an assets and set a timezone', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, dateTimeOriginal: '2023-11-19T18:11:00' });
|
||||
|
||||
await sut.updateAll(auth, { ids: [asset.id], dateTimeRelative: -11, timeZone: 'UTC+5' });
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id, { exifInfo: true })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2023-11-19T18:00:00+00:00',
|
||||
timeZone: 'UTC+5',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should update dateTimeOriginal', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queueAll.mockResolvedValue();
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto';
|
||||
import { AssetFaceCreateDto } from 'src/dtos/person.dto';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { PersonRepository } from 'src/repositories/person.repository';
|
||||
@@ -15,7 +19,7 @@ let defaultDatabase: Kysely<DB>;
|
||||
const setup = (db?: Kysely<DB>) => {
|
||||
return newMediumService(PersonService, {
|
||||
database: db || defaultDatabase,
|
||||
real: [AccessRepository, DatabaseRepository, PersonRepository],
|
||||
real: [AccessRepository, DatabaseRepository, PersonRepository, AssetRepository, AssetEditRepository],
|
||||
mock: [LoggingRepository, StorageRepository],
|
||||
});
|
||||
};
|
||||
@@ -77,4 +81,609 @@ describe(PersonService.name, () => {
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(person2.thumbnailPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFace', () => {
|
||||
it('should store and retrieve the face as-is when there are no edits', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 200 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 });
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 200,
|
||||
imageHeight: 200,
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 150,
|
||||
height: 150,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
// retrieve an asset's faces
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 50,
|
||||
boundingBoxY1: 50,
|
||||
boundingBoxX2: 200,
|
||||
boundingBoxY2: 200,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly transform the coordinates when the asset is edited (Crop)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 200 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 });
|
||||
|
||||
await ctx.newEdits(asset.id, {
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Crop,
|
||||
parameters: {
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 150,
|
||||
height: 200,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 150,
|
||||
imageHeight: 200,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
// retrieve an asset's faces
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 0,
|
||||
boundingBoxY1: 0,
|
||||
boundingBoxX2: 100,
|
||||
boundingBoxY2: 100,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// remove edits and verify the stored coordinates map to the original image
|
||||
await ctx.newEdits(asset.id, { edits: [] });
|
||||
|
||||
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(facesAfterRemovingEdits).resolves.toHaveLength(1);
|
||||
await expect(facesAfterRemovingEdits).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 50,
|
||||
boundingBoxY1: 50,
|
||||
boundingBoxX2: 150,
|
||||
boundingBoxY2: 150,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly transform the coordinates when the asset is edited (Rotate 90)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 200 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageWidth: 200, exifImageHeight: 100 });
|
||||
|
||||
await ctx.newEdits(asset.id, {
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Rotate,
|
||||
parameters: {
|
||||
angle: 90,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 100,
|
||||
imageHeight: 200,
|
||||
x: 25,
|
||||
y: 50,
|
||||
width: 10,
|
||||
height: 10,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: expect.closeTo(25, 1),
|
||||
boundingBoxY1: expect.closeTo(50, 1),
|
||||
boundingBoxX2: expect.closeTo(35, 1),
|
||||
boundingBoxY2: expect.closeTo(60, 1),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// remove edits and verify the stored coordinates map to the original image
|
||||
await ctx.newEdits(asset.id, { edits: [] });
|
||||
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(facesAfterRemovingEdits).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 50,
|
||||
boundingBoxY1: 65,
|
||||
boundingBoxX2: 60,
|
||||
boundingBoxY2: 75,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly transform the coordinates when the asset is edited (Mirror Horizontal)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 100 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 200 });
|
||||
|
||||
await ctx.newEdits(asset.id, {
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Mirror,
|
||||
parameters: {
|
||||
axis: MirrorAxis.Horizontal,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 200,
|
||||
imageHeight: 100,
|
||||
x: 50,
|
||||
y: 25,
|
||||
width: 100,
|
||||
height: 50,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 50,
|
||||
boundingBoxY1: 25,
|
||||
boundingBoxX2: 150,
|
||||
boundingBoxY2: 75,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// remove edits and verify the stored coordinates map to the original image
|
||||
await ctx.newEdits(asset.id, { edits: [] });
|
||||
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(facesAfterRemovingEdits).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 50,
|
||||
boundingBoxY1: 25,
|
||||
boundingBoxX2: 150,
|
||||
boundingBoxY2: 75,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly transform the coordinates when the asset is edited (Crop + Rotate)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 150 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 });
|
||||
|
||||
await ctx.newEdits(asset.id, {
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Crop,
|
||||
parameters: {
|
||||
x: 50,
|
||||
y: 0,
|
||||
width: 150,
|
||||
height: 200,
|
||||
},
|
||||
},
|
||||
{
|
||||
action: AssetEditAction.Rotate,
|
||||
parameters: {
|
||||
angle: 90,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 200,
|
||||
imageHeight: 150,
|
||||
x: 50,
|
||||
y: 25,
|
||||
width: 10,
|
||||
height: 20,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: expect.closeTo(50, 1),
|
||||
boundingBoxY1: expect.closeTo(25, 1),
|
||||
boundingBoxX2: expect.closeTo(60, 1),
|
||||
boundingBoxY2: expect.closeTo(45, 1),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// remove edits and verify the stored coordinates map to the original image
|
||||
await ctx.newEdits(asset.id, { edits: [] });
|
||||
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(facesAfterRemovingEdits).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 75,
|
||||
boundingBoxY1: 140,
|
||||
boundingBoxX2: 95,
|
||||
boundingBoxY2: 150,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly transform the coordinates when the asset is edited (Crop + Mirror)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 100 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 200 });
|
||||
|
||||
await ctx.newEdits(asset.id, {
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Crop,
|
||||
parameters: {
|
||||
x: 50,
|
||||
y: 0,
|
||||
width: 150,
|
||||
height: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
action: AssetEditAction.Mirror,
|
||||
parameters: {
|
||||
axis: MirrorAxis.Horizontal,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 150,
|
||||
imageHeight: 100,
|
||||
x: 25,
|
||||
y: 25,
|
||||
width: 75,
|
||||
height: 50,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 25,
|
||||
boundingBoxY1: 25,
|
||||
boundingBoxX2: 100,
|
||||
boundingBoxY2: 75,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// remove edits and verify the stored coordinates map to the original image
|
||||
await ctx.newEdits(asset.id, { edits: [] });
|
||||
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(facesAfterRemovingEdits).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 100,
|
||||
boundingBoxY1: 25,
|
||||
boundingBoxX2: 175,
|
||||
boundingBoxY2: 75,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly transform the coordinates when the asset is edited (Rotate + Mirror)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 200, height: 150 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 150 });
|
||||
|
||||
await ctx.newEdits(asset.id, {
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Rotate,
|
||||
parameters: {
|
||||
angle: 90,
|
||||
},
|
||||
},
|
||||
{
|
||||
action: AssetEditAction.Mirror,
|
||||
parameters: {
|
||||
axis: MirrorAxis.Horizontal,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 200,
|
||||
imageHeight: 150,
|
||||
x: 50,
|
||||
y: 25,
|
||||
width: 15,
|
||||
height: 20,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: expect.closeTo(50, 1),
|
||||
boundingBoxY1: expect.closeTo(25, 1),
|
||||
boundingBoxX2: expect.closeTo(65, 1),
|
||||
boundingBoxY2: expect.closeTo(45, 1),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// remove edits and verify the stored coordinates map to the original image
|
||||
await ctx.newEdits(asset.id, { edits: [] });
|
||||
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(facesAfterRemovingEdits).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 25,
|
||||
boundingBoxY1: 50,
|
||||
boundingBoxX2: 45,
|
||||
boundingBoxY2: 65,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly transform the coordinates when the asset is edited (Crop + Rotate + Mirror)', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 150, height: 100 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 200, exifImageWidth: 200 });
|
||||
|
||||
await ctx.newEdits(asset.id, {
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Crop,
|
||||
parameters: {
|
||||
x: 50,
|
||||
y: 25,
|
||||
width: 100,
|
||||
height: 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
action: AssetEditAction.Rotate,
|
||||
parameters: {
|
||||
angle: 270,
|
||||
},
|
||||
},
|
||||
{
|
||||
action: AssetEditAction.Mirror,
|
||||
parameters: {
|
||||
axis: MirrorAxis.Horizontal,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 150,
|
||||
imageHeight: 150,
|
||||
x: 25,
|
||||
y: 50,
|
||||
width: 75,
|
||||
height: 50,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: expect.closeTo(25, 1),
|
||||
boundingBoxY1: expect.closeTo(50, 1),
|
||||
boundingBoxX2: expect.closeTo(100, 1),
|
||||
boundingBoxY2: expect.closeTo(100, 1),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// remove edits and verify the stored coordinates map to the original image
|
||||
await ctx.newEdits(asset.id, { edits: [] });
|
||||
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(facesAfterRemovingEdits).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 50,
|
||||
boundingBoxY1: 75,
|
||||
boundingBoxX2: 100,
|
||||
boundingBoxY2: 150,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly transform the coordinates with multiple mirrors in sequence', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { person } = await ctx.newPerson({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ id: factory.uuid(), ownerId: user.id, width: 100, height: 100 });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 100 });
|
||||
|
||||
await ctx.newEdits(asset.id, {
|
||||
edits: [
|
||||
{
|
||||
action: AssetEditAction.Mirror,
|
||||
parameters: {
|
||||
axis: MirrorAxis.Horizontal,
|
||||
},
|
||||
},
|
||||
{
|
||||
action: AssetEditAction.Mirror,
|
||||
parameters: {
|
||||
axis: MirrorAxis.Vertical,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const dto: AssetFaceCreateDto = {
|
||||
imageWidth: 100,
|
||||
imageHeight: 100,
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 80,
|
||||
height: 80,
|
||||
personId: person.id,
|
||||
assetId: asset.id,
|
||||
};
|
||||
|
||||
await sut.createFace(auth, dto);
|
||||
|
||||
const faces = sut.getFacesById(auth, { id: asset.id });
|
||||
await expect(faces).resolves.toHaveLength(1);
|
||||
await expect(faces).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 10,
|
||||
boundingBoxY1: 10,
|
||||
boundingBoxX2: 90,
|
||||
boundingBoxY2: 90,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// remove edits and verify the stored coordinates map to the original image
|
||||
await ctx.newEdits(asset.id, { edits: [] });
|
||||
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
|
||||
|
||||
await expect(facesAfterRemovingEdits).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
person: expect.objectContaining({ id: person.id }),
|
||||
boundingBoxX1: 10,
|
||||
boundingBoxY1: 10,
|
||||
boundingBoxX2: 90,
|
||||
boundingBoxY2: 90,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -361,6 +361,7 @@ const assetSidecarWriteFactory = () => {
|
||||
latitude: 12,
|
||||
longitude: 12,
|
||||
dateTimeOriginal: '2023-11-22T04:56:12.196Z',
|
||||
timeZone: 'UTC-6',
|
||||
} as unknown as Exif,
|
||||
};
|
||||
};
|
||||
@@ -532,6 +533,7 @@ export const factory = {
|
||||
assetEdit: assetEditFactory,
|
||||
tag: tagFactory,
|
||||
uuid: newUuid,
|
||||
buffer: () => Buffer.from('this is a fake buffer'),
|
||||
date: newDate,
|
||||
responses: {
|
||||
badRequest: (message: any = null) => ({
|
||||
|
||||
@@ -2,9 +2,6 @@ import swc from 'unplugin-swc';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
// Set the timezone to UTC to avoid timezone issues during testing
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
root: './',
|
||||
@@ -25,6 +22,9 @@ export default defineConfig({
|
||||
fallbackCJS: true,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
TZ: 'UTC',
|
||||
},
|
||||
},
|
||||
plugins: [swc.vite(), tsconfigPaths()],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user