chore: refactor test factories (#26804)

This commit is contained in:
Daniel Dietzler 2026-03-09 21:47:03 +01:00 committed by GitHub
parent f2726606e0
commit d325231df2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 79 additions and 178 deletions

View File

@ -532,7 +532,7 @@ describe(AssetService.name, () => {
});
it('should immediately queue assets for deletion if trash is disabled', async () => {
const asset = factory.asset({ isOffline: false });
const asset = AssetFactory.create();
mocks.assetJob.streamForDeletedJob.mockReturnValue(makeStream([asset]));
mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: false } });
@ -546,7 +546,7 @@ describe(AssetService.name, () => {
});
it('should queue assets for deletion after trash duration', async () => {
const asset = factory.asset({ isOffline: false });
const asset = AssetFactory.create();
mocks.assetJob.streamForDeletedJob.mockReturnValue(makeStream([asset]));
mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: true, days: 7 } });
@ -739,7 +739,7 @@ describe(AssetService.name, () => {
describe('upsertMetadata', () => {
it('should throw a bad request exception if duplicate keys are sent', async () => {
const asset = factory.asset();
const asset = AssetFactory.create();
const items = [
{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } },
{ key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } },
@ -757,7 +757,7 @@ describe(AssetService.name, () => {
describe('upsertBulkMetadata', () => {
it('should throw a bad request exception if duplicate keys are sent', async () => {
const asset = factory.asset();
const asset = AssetFactory.create();
const items = [
{ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } },
{ assetId: asset.id, key: AssetMetadataKey.MobileApp, value: { iCloudId: 'id1' } },

View File

@ -1,6 +1,8 @@
import { BadRequestException } from '@nestjs/common';
import { MemoryService } from 'src/services/memory.service';
import { OnThisDayData } from 'src/types';
import { AssetFactory } from 'test/factories/asset.factory';
import { MemoryFactory } from 'test/factories/memory.factory';
import { factory, newUuid, newUuids } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
@ -27,9 +29,9 @@ describe(MemoryService.name, () => {
describe('search', () => {
it('should search memories', async () => {
const [userId] = newUuids();
const asset = factory.asset();
const memory1 = factory.memory({ ownerId: userId, assets: [asset] });
const memory2 = factory.memory({ ownerId: userId });
const asset = AssetFactory.create();
const memory1 = MemoryFactory.from({ ownerId: userId }).asset(asset).build();
const memory2 = MemoryFactory.create({ ownerId: userId });
mocks.memory.search.mockResolvedValue([memory1, memory2]);
@ -64,7 +66,7 @@ describe(MemoryService.name, () => {
it('should get a memory by id', async () => {
const userId = newUuid();
const memory = factory.memory({ ownerId: userId });
const memory = MemoryFactory.create({ ownerId: userId });
mocks.memory.get.mockResolvedValue(memory);
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
@ -81,7 +83,7 @@ describe(MemoryService.name, () => {
describe('create', () => {
it('should skip assets the user does not have access to', async () => {
const [assetId, userId] = newUuids();
const memory = factory.memory({ ownerId: userId });
const memory = MemoryFactory.create({ ownerId: userId });
mocks.memory.create.mockResolvedValue(memory);
@ -109,8 +111,8 @@ describe(MemoryService.name, () => {
it('should create a memory', async () => {
const [assetId, userId] = newUuids();
const asset = factory.asset({ id: assetId, ownerId: userId });
const memory = factory.memory({ assets: [asset] });
const asset = AssetFactory.create({ id: assetId, ownerId: userId });
const memory = MemoryFactory.from().asset(asset).build();
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
mocks.memory.create.mockResolvedValue(memory);
@ -131,7 +133,7 @@ describe(MemoryService.name, () => {
});
it('should create a memory without assets', async () => {
const memory = factory.memory();
const memory = MemoryFactory.create();
mocks.memory.create.mockResolvedValue(memory);
@ -155,7 +157,7 @@ describe(MemoryService.name, () => {
});
it('should update a memory', async () => {
const memory = factory.memory();
const memory = MemoryFactory.create();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.memory.update.mockResolvedValue(memory);
@ -198,7 +200,7 @@ describe(MemoryService.name, () => {
it('should require asset access', async () => {
const assetId = newUuid();
const memory = factory.memory();
const memory = MemoryFactory.create();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.memory.get.mockResolvedValue(memory);
@ -212,8 +214,8 @@ describe(MemoryService.name, () => {
});
it('should skip assets already in the memory', async () => {
const asset = factory.asset();
const memory = factory.memory({ assets: [asset] });
const asset = AssetFactory.create();
const memory = MemoryFactory.from().asset(asset).build();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.memory.get.mockResolvedValue(memory);
@ -228,7 +230,7 @@ describe(MemoryService.name, () => {
it('should add assets', async () => {
const assetId = newUuid();
const memory = factory.memory();
const memory = MemoryFactory.create();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId]));
@ -266,8 +268,8 @@ describe(MemoryService.name, () => {
});
it('should remove assets', async () => {
const memory = factory.memory();
const asset = factory.asset();
const memory = MemoryFactory.create();
const asset = AssetFactory.create();
mocks.access.memory.checkOwnerAccess.mockResolvedValue(new Set([memory.id]));
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));

View File

@ -0,0 +1,45 @@
import { Selectable } from 'kysely';
import { MemoryType } from 'src/enum';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { AssetFactory } from 'test/factories/asset.factory';
import { build } from 'test/factories/builder.factory';
import { AssetLike, FactoryBuilder, MemoryLike } from 'test/factories/types';
import { newDate, newUuid, newUuidV7 } from 'test/small.factory';
export class MemoryFactory {
#assets: AssetFactory[] = [];
private constructor(private readonly value: Selectable<MemoryTable>) {}
static create(dto: MemoryLike = {}) {
return MemoryFactory.from(dto).build();
}
static from(dto: MemoryLike = {}) {
return new MemoryFactory({
id: newUuid(),
createdAt: newDate(),
updatedAt: newDate(),
updateId: newUuidV7(),
deletedAt: null,
ownerId: newUuid(),
type: MemoryType.OnThisDay,
data: { year: 2024 },
isSaved: false,
memoryAt: newDate(),
seenAt: null,
showAt: newDate(),
hideAt: newDate(),
...dto,
});
}
asset(asset: AssetLike, builder?: FactoryBuilder<AssetFactory>) {
this.#assets.push(build(AssetFactory.from(asset), builder));
return this;
}
build() {
return { ...this.value, assets: this.#assets.map((asset) => asset.build()) };
}
}

View File

@ -6,6 +6,7 @@ import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
import { AssetFaceTable } from 'src/schema/tables/asset-face.table';
import { AssetFileTable } from 'src/schema/tables/asset-file.table';
import { AssetTable } from 'src/schema/tables/asset.table';
import { MemoryTable } from 'src/schema/tables/memory.table';
import { PersonTable } from 'src/schema/tables/person.table';
import { SharedLinkTable } from 'src/schema/tables/shared-link.table';
import { StackTable } from 'src/schema/tables/stack.table';
@ -24,3 +25,4 @@ export type UserLike = Partial<Selectable<UserTable>>;
export type AssetFaceLike = Partial<Selectable<AssetFaceTable>>;
export type PersonLike = Partial<Selectable<PersonTable>>;
export type StackLike = Partial<Selectable<StackTable>>;
export type MemoryLike = Partial<Selectable<MemoryTable>>;

View File

@ -2,39 +2,24 @@ import {
Activity,
Album,
ApiKey,
AssetFace,
AssetFile,
AuthApiKey,
AuthSharedLink,
AuthUser,
Exif,
Library,
Memory,
Partner,
Person,
Session,
Stack,
Tag,
User,
UserAdmin,
} from 'src/database';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetEditAction, AssetEditActionItem, MirrorAxis } from 'src/dtos/editing.dto';
import { QueueStatisticsDto } from 'src/dtos/queue.dto';
import {
AssetFileType,
AssetOrder,
AssetStatus,
AssetType,
AssetVisibility,
MemoryType,
Permission,
SourceType,
UserMetadataKey,
UserStatus,
} from 'src/enum';
import { DeepPartial, OnThisDayData, UserMetadataItem } from 'src/types';
import { AssetFileType, AssetOrder, Permission, UserMetadataKey, UserStatus } from 'src/enum';
import { UserMetadataItem } from 'src/types';
import { UserFactory } from 'test/factories/user.factory';
import { v4, v7 } from 'uuid';
export const newUuid = () => v4();
@ -123,9 +108,13 @@ const authUserFactory = (authUser: Partial<AuthUser> = {}) => {
return { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes };
};
const partnerFactory = (partner: Partial<Partner> = {}) => {
const sharedBy = userFactory(partner.sharedBy || {});
const sharedWith = userFactory(partner.sharedWith || {});
const partnerFactory = ({
sharedBy: sharedByProvided,
sharedWith: sharedWithProvided,
...partner
}: Partial<Partner> = {}) => {
const sharedBy = UserFactory.create(sharedByProvided ?? {});
const sharedWith = UserFactory.create(sharedWithProvided ?? {});
return {
sharedById: sharedBy.id,
@ -168,19 +157,6 @@ const queueStatisticsFactory = (dto?: Partial<QueueStatisticsDto>) => ({
...dto,
});
const stackFactory = ({ owner, assets, ...stack }: DeepPartial<Stack> = {}): Stack => {
const ownerId = newUuid();
return {
id: newUuid(),
primaryAssetId: assets?.[0].id ?? newUuid(),
ownerId,
owner: userFactory(owner ?? { id: ownerId }),
assets: assets?.map((asset) => assetFactory(asset)) ?? [],
...stack,
};
};
const userFactory = (user: Partial<User> = {}) => ({
id: newUuid(),
name: 'Test User',
@ -238,44 +214,6 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
};
};
const assetFactory = (
asset: Omit<DeepPartial<MapAsset>, 'exifInfo' | 'owner' | 'stack' | 'tags' | 'faces' | 'files' | 'edits'> = {},
) => {
return {
id: newUuid(),
createdAt: newDate(),
updatedAt: newDate(),
deletedAt: null,
updateId: newUuidV7(),
status: AssetStatus.Active,
checksum: newSha1(),
deviceAssetId: '',
deviceId: '',
duplicateId: null,
duration: null,
encodedVideoPath: null,
fileCreatedAt: newDate(),
fileModifiedAt: newDate(),
isExternal: false,
isFavorite: false,
isOffline: false,
libraryId: null,
livePhotoVideoId: null,
localDateTime: newDate(),
originalFileName: 'IMG_123.jpg',
originalPath: `/data/12/34/IMG_123.jpg`,
ownerId: newUuid(),
stackId: null,
thumbhash: null,
type: AssetType.Image,
visibility: AssetVisibility.Timeline,
width: null,
height: null,
isEdited: false,
...asset,
};
};
const activityFactory = (activity: Partial<Activity> = {}) => {
const userId = activity.userId || newUuid();
return {
@ -283,7 +221,7 @@ const activityFactory = (activity: Partial<Activity> = {}) => {
comment: null,
isLiked: false,
userId,
user: userFactory({ id: userId }),
user: UserFactory.create({ id: userId }),
assetId: newUuid(),
albumId: newUuid(),
createdAt: newDate(),
@ -319,24 +257,6 @@ const libraryFactory = (library: Partial<Library> = {}) => ({
...library,
});
const memoryFactory = (memory: Partial<Memory> = {}) => ({
id: newUuid(),
createdAt: newDate(),
updatedAt: newDate(),
updateId: newUuidV7(),
deletedAt: null,
ownerId: newUuid(),
type: MemoryType.OnThisDay,
data: { year: 2024 } as OnThisDayData,
isSaved: false,
memoryAt: newDate(),
seenAt: null,
showAt: newDate(),
hideAt: newDate(),
assets: [],
...memory,
});
const versionHistoryFactory = () => ({
id: newUuid(),
createdAt: newDate(),
@ -403,49 +323,6 @@ const assetOcrFactory = (
...ocr,
});
const assetFileFactory = (file: Partial<AssetFile> = {}) => ({
id: newUuid(),
type: AssetFileType.Preview,
path: '/uploads/user-id/thumbs/path.jpg',
isEdited: false,
isProgressive: false,
...file,
});
const exifFactory = (exif: Partial<Exif> = {}) => ({
assetId: newUuid(),
autoStackId: null,
bitsPerSample: null,
city: 'Austin',
colorspace: null,
country: 'United States of America',
dateTimeOriginal: newDate(),
description: '',
exifImageHeight: 420,
exifImageWidth: 42,
exposureTime: null,
fileSizeInByte: 69,
fNumber: 1.7,
focalLength: 4.38,
fps: null,
iso: 947,
latitude: 30.267_334_570_570_195,
longitude: -97.789_833_534_282_07,
lensModel: null,
livePhotoCID: null,
make: 'Google',
model: 'Pixel 7',
modifyDate: newDate(),
orientation: '1',
profileDescription: null,
projectionType: null,
rating: 4,
state: 'Texas',
tags: ['parent/child'],
timeZone: 'UTC-6',
...exif,
});
const tagFactory = (tag: Partial<Tag>): Tag => ({
id: newUuid(),
color: null,
@ -456,25 +333,6 @@ const tagFactory = (tag: Partial<Tag>): Tag => ({
...tag,
});
const faceFactory = ({ person, ...face }: DeepPartial<AssetFace> = {}): AssetFace => ({
assetId: newUuid(),
boundingBoxX1: 1,
boundingBoxX2: 2,
boundingBoxY1: 1,
boundingBoxY2: 2,
deletedAt: null,
id: newUuid(),
imageHeight: 420,
imageWidth: 42,
isVisible: true,
personId: null,
sourceType: SourceType.MachineLearning,
updatedAt: newDate(),
updateId: newUuidV7(),
person: person === null ? null : personFactory(person),
...face,
});
const assetEditFactory = (edit?: Partial<AssetEditActionItem>): AssetEditActionItem => {
switch (edit?.action) {
case AssetEditAction.Crop: {
@ -529,26 +387,20 @@ const albumFactory = (album?: Partial<Omit<Album, 'assets'>>) => ({
export const factory = {
activity: activityFactory,
apiKey: apiKeyFactory,
asset: assetFactory,
assetFile: assetFileFactory,
assetOcr: assetOcrFactory,
auth: authFactory,
authApiKey: authApiKeyFactory,
authUser: authUserFactory,
library: libraryFactory,
memory: memoryFactory,
partner: partnerFactory,
queueStatistics: queueStatisticsFactory,
session: sessionFactory,
stack: stackFactory,
user: userFactory,
userAdmin: userAdminFactory,
versionHistory: versionHistoryFactory,
jobAssets: {
sidecarWrite: assetSidecarWriteFactory,
},
exif: exifFactory,
face: faceFactory,
person: personFactory,
assetEdit: assetEditFactory,
tag: tagFactory,