diff --git a/server/src/services/map.service.spec.ts b/server/src/services/map.service.spec.ts index 6dc56abf44..e7369569d2 100644 --- a/server/src/services/map.service.spec.ts +++ b/server/src/services/map.service.spec.ts @@ -1,7 +1,8 @@ import { MapService } from 'src/services/map.service'; -import { albumStub } from 'test/fixtures/album.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; +import { userStub } from 'test/fixtures/user.stub'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -72,8 +73,8 @@ describe(MapService.name, () => { }; mocks.partner.getAll.mockResolvedValue([]); mocks.map.getMapMarkers.mockResolvedValue([marker]); - mocks.album.getOwned.mockResolvedValue([albumStub.empty]); - mocks.album.getShared.mockResolvedValue([albumStub.sharedWithUser]); + mocks.album.getOwned.mockResolvedValue([AlbumFactory.create()]); + mocks.album.getShared.mockResolvedValue([AlbumFactory.from().albumUser({ userId: userStub.user1.id }).build()]); const markers = await sut.getMapMarkers(authStub.user1, { withSharedAlbums: true }); diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 6627ffea8a..bece89d73e 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,14 +1,15 @@ import { plainToInstance } from 'class-transformer'; import { defaults, SystemConfig } from 'src/config'; -import { AlbumUser } from 'src/database'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; import { NotificationService } from 'src/services/notification.service'; import { INotifyAlbumUpdateJob } from 'src/types'; -import { albumStub } from 'test/fixtures/album.stub'; -import { assetStub } from 'test/fixtures/asset.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFileFactory } from 'test/factories/asset-file.factory'; +import { UserFactory } from 'test/factories/user.factory'; import { notificationStub } from 'test/fixtures/notification.stub'; import { userStub } from 'test/fixtures/user.stub'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; const configs = { @@ -267,14 +268,14 @@ describe(NotificationService.name, () => { }); it('should skip if recipient could not be found', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Skipped); expect(mocks.job.queue).not.toHaveBeenCalled(); }); it('should skip if the recipient has email notifications disabled', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -290,7 +291,7 @@ describe(NotificationService.name, () => { }); it('should skip if the recipient has email notifications for album invite disabled', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -306,7 +307,7 @@ describe(NotificationService.name, () => { }); it('should send invite email', async () => { - mocks.album.getById.mockResolvedValue(albumStub.empty); + mocks.album.getById.mockResolvedValue(AlbumFactory.create()); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -328,7 +329,8 @@ describe(NotificationService.name, () => { }); it('should send invite email without album thumbnail if thumbnail asset does not exist', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const album = AlbumFactory.create({ albumThumbnailAssetId: newUuid() }); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -345,7 +347,7 @@ describe(NotificationService.name, () => { await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -358,7 +360,9 @@ describe(NotificationService.name, () => { }); it('should send invite email with album thumbnail as jpeg', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const assetFile = AssetFileFactory.create({ type: AssetFileType.Thumbnail }); + const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId }); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -371,13 +375,11 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([ - { id: '1', type: AssetFileType.Thumbnail, path: 'path-to-thumb.jpg', isEdited: false }, - ]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetFile]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -390,7 +392,9 @@ describe(NotificationService.name, () => { }); it('should send invite email with album thumbnail and arbitrary extension', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + const assetFile = AssetFileFactory.create({ path: 'some-thumb.ext', type: AssetFileType.Thumbnail }); + const album = AlbumFactory.create({ albumThumbnailAssetId: assetFile.assetId }); + mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue({ ...userStub.user1, metadata: [ @@ -403,11 +407,11 @@ describe(NotificationService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ server: {} }); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); - mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([{ ...assetStub.image.files[2], isEdited: false }]); + mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([assetFile]); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.Success); expect(mocks.assetJob.getAlbumThumbnailFiles).toHaveBeenCalledWith( - albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, + album.albumThumbnailAssetId, AssetFileType.Thumbnail, ); expect(mocks.job.queue).toHaveBeenCalledWith({ @@ -427,85 +431,74 @@ describe(NotificationService.name, () => { }); it('should skip if owner could not be found', async () => { - mocks.album.getById.mockResolvedValue(albumStub.emptyWithValidThumbnail); + mocks.album.getById.mockResolvedValue(AlbumFactory.create({ ownerId: 'non-existent' })); await expect(sut.handleAlbumUpdate({ id: '', recipientId: '1' })).resolves.toBe(JobStatus.Skipped); expect(mocks.systemMetadata.get).not.toHaveBeenCalled(); }); it('should skip recipient that could not be looked up', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValueOnce(userStub.user1); + const album = AlbumFactory.from().albumUser({ userId: 'non-existent' }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValueOnce(album.owner); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: 'non-existent' }); + expect(mocks.user.get).toHaveBeenCalledWith('non-existent', { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue({ - ...userStub.user1, - metadata: [ - { - key: UserMetadataKey.Preferences, - value: { emailNotifications: { enabled: false, albumUpdate: true } }, - }, - ], - }); + const user = UserFactory.from() + .metadata({ + key: UserMetadataKey.Preferences, + value: { emailNotifications: { enabled: false, albumUpdate: true } }, + }) + .build(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should skip recipient with disabled email notifications for the album update event', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue({ - ...userStub.user1, - metadata: [ - { - key: UserMetadataKey.Preferences, - value: { emailNotifications: { enabled: true, albumUpdate: false } }, - }, - ], - }); + const user = UserFactory.from() + .metadata({ + key: UserMetadataKey.Preferences, + value: { emailNotifications: { enabled: true, albumUpdate: false } }, + }) + .build(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).not.toHaveBeenCalled(); }); it('should send email', async () => { - mocks.album.getById.mockResolvedValue({ - ...albumStub.emptyWithValidThumbnail, - albumUsers: [{ user: { id: userStub.user1.id } } as AlbumUser], - }); - mocks.user.get.mockResolvedValue(userStub.user1); + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); + mocks.album.getById.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(user); mocks.notification.create.mockResolvedValue(notificationStub.albumEvent); mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' }); mocks.assetJob.getAlbumThumbnailFiles.mockResolvedValue([]); - await sut.handleAlbumUpdate({ id: '', recipientId: userStub.user1.id }); - expect(mocks.user.get).toHaveBeenCalledWith(userStub.user1.id, { withDeleted: false }); + await sut.handleAlbumUpdate({ id: '', recipientId: user.id }); + expect(mocks.user.get).toHaveBeenCalledWith(user.id, { withDeleted: false }); expect(mocks.email.renderEmail).toHaveBeenCalled(); expect(mocks.job.queue).toHaveBeenCalled(); }); diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 90c212650e..8c0e336d5b 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { SharedLinkType } from 'src/enum'; import { SharedLinkService } from 'src/services/shared-link.service'; -import { albumStub } from 'test/fixtures/album.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { sharedLinkResponseStub, sharedLinkStub } from 'test/fixtures/shared-link.stub'; @@ -120,19 +120,17 @@ describe(SharedLinkService.name, () => { }); it('should create an album shared link', async () => { - mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); + const album = AlbumFactory.from().asset().build(); + mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.sharedLink.create.mockResolvedValue(sharedLinkStub.valid); - await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: albumStub.oneAsset.id }); + await sut.create(authStub.admin, { type: SharedLinkType.Album, albumId: album.id }); - expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith( - authStub.admin.user.id, - new Set([albumStub.oneAsset.id]), - ); + expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set([album.id])); expect(mocks.sharedLink.create).toHaveBeenCalledWith({ type: SharedLinkType.Album, userId: authStub.admin.user.id, - albumId: albumStub.oneAsset.id, + albumId: album.id, allowDownload: true, allowUpload: true, description: null, diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 0b5d538cea..2b3e9d3f9f 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -2,7 +2,7 @@ import { Stats } from 'node:fs'; import { defaults, SystemConfig } from 'src/config'; import { AssetPathType, JobStatus } from 'src/enum'; import { StorageTemplateService } from 'src/services/storage-template.service'; -import { albumStub } from 'test/fixtures/album.stub'; +import { AlbumFactory } from 'test/factories/album.factory'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; import { factory } from 'test/small.factory'; @@ -143,7 +143,7 @@ describe(StorageTemplateService.name, () => { it('should use handlebar if condition for album', async () => { const asset = assetStub.storageAsset(); const user = userStub.user1; - const album = albumStub.oneAsset; + const album = AlbumFactory.from().asset().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}'; @@ -191,7 +191,7 @@ describe(StorageTemplateService.name, () => { it('should handle album startDate', async () => { const asset = assetStub.storageAsset(); const user = userStub.user1; - const album = albumStub.oneAsset; + const album = AlbumFactory.from().asset().build(); const config = structuredClone(defaults); config.storageTemplate.template = '{{#if album}}{{album-startDate-y}}/{{album-startDate-MM}} - {{album}}{{else}}{{y}}/{{MM}}/{{/if}}/{{filename}}'; diff --git a/server/test/factories/user.factory.ts b/server/test/factories/user.factory.ts index e6e84d94a1..125ce91e86 100644 --- a/server/test/factories/user.factory.ts +++ b/server/test/factories/user.factory.ts @@ -6,6 +6,8 @@ import { UserLike } from 'test/factories/types'; import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; export class UserFactory { + #metadata: Selectable[] = []; + private constructor(private value: Selectable) {} static create(dto: UserLike = {}) { @@ -37,10 +39,21 @@ export class UserFactory { }); } + metadata(dto: Partial> & Pick, 'key' | 'value'>) { + this.#metadata.push({ + updatedAt: newDate(), + updateId: newUuid(), + userId: newUuid(), + ...dto, + }); + + return this; + } + build() { return { ...this.value, - metadata: [] as UserMetadataTable[], + metadata: this.#metadata, }; } } diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts deleted file mode 100644 index 9480fdd5ab..0000000000 --- a/server/test/fixtures/album.stub.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { AlbumUserRole, AssetOrder } from 'src/enum'; -import { assetStub } from 'test/fixtures/asset.stub'; -import { authStub } from 'test/fixtures/auth.stub'; -import { userStub } from 'test/fixtures/user.stub'; - -export const albumStub = { - empty: Object.freeze({ - id: 'album-1', - albumName: 'Empty album', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - sharedWithUser: Object.freeze({ - id: 'album-2', - albumName: 'Empty album shared with user', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [ - { - user: userStub.user1, - role: AlbumUserRole.Editor, - }, - ], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - oneAsset: Object.freeze({ - id: 'album-4', - albumName: 'Album with one asset', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [assetStub.image], - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), - emptyWithValidThumbnail: Object.freeze({ - id: 'album-5', - albumName: 'Empty album with valid thumbnail', - description: '', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - assets: [], - albumThumbnailAsset: assetStub.image, - albumThumbnailAssetId: assetStub.image.id, - createdAt: new Date(), - updatedAt: new Date(), - deletedAt: null, - sharedLinks: [], - albumUsers: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - updateId: '42', - }), -};