chore: wip tests

This commit is contained in:
bwees
2026-02-03 11:55:06 -06:00
parent 8ebba759d3
commit be67147766
2 changed files with 692 additions and 1 deletions

View File

@@ -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> {

View File

@@ -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,685 @@ 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.get(AssetEditRepository).replaceAll(asset.id, []);
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.only('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: 200, height: 100 });
await ctx.newExif({ assetId: asset.id, exifImageHeight: 100, exifImageWidth: 200 });
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Rotate,
parameters: {
angle: 90,
},
},
],
});
const auth = factory.auth({ user });
const dto: AssetFaceCreateDto = {
imageWidth: 200,
imageHeight: 100,
x: 25,
y: 50,
width: 50,
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: 50,
boundingBoxX2: 75,
boundingBoxY2: 100,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.get(AssetEditRepository).replaceAll(asset.id, []);
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 (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.get(AssetEditRepository).replaceAll(asset.id, []);
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: 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: 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: 100,
height: 75,
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: 100,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.get(AssetEditRepository).replaceAll(asset.id, []);
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: 50,
boundingBoxX2: 150,
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.get(AssetEditRepository).replaceAll(asset.id, []);
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: 150, height: 200 });
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: 100,
height: 75,
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: 100,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.get(AssetEditRepository).replaceAll(asset.id, []);
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: 50,
boundingBoxX2: 150,
boundingBoxY2: 150,
}),
]),
);
});
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: 100, 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: 25,
width: 150,
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: 25,
boundingBoxY1: 50,
boundingBoxX2: 100,
boundingBoxY2: 100,
}),
]),
);
// remove edits and verify the stored coordinates map to the original image
await ctx.get(AssetEditRepository).replaceAll(asset.id, []);
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: 75,
boundingBoxX2: 150,
boundingBoxY2: 150,
}),
]),
);
});
it('should properly transform the coordinates with multiple crops 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: 200, exifImageWidth: 200 });
await ctx.newEdits(asset.id, {
edits: [
{
action: AssetEditAction.Crop,
parameters: {
x: 50,
y: 50,
width: 150,
height: 150,
},
},
{
action: AssetEditAction.Crop,
parameters: {
x: 25,
y: 25,
width: 100,
height: 100,
},
},
],
});
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.get(AssetEditRepository).replaceAll(asset.id, []);
const facesAfterRemovingEdits = sut.getFacesById(auth, { id: asset.id });
await expect(facesAfterRemovingEdits).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({
person: expect.objectContaining({ id: person.id }),
boundingBoxX1: 85,
boundingBoxY1: 85,
boundingBoxX2: 165,
boundingBoxY2: 165,
}),
]),
);
});
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.get(AssetEditRepository).replaceAll(asset.id, []);
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,
}),
]),
);
});
});
});