From b3820c259e29e599f6211df592a7da816424cac1 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 6 Feb 2026 16:32:50 -0500 Subject: [PATCH] refactor: test factories (#25977) --- server/src/queries/album.repository.sql | 6 +- server/src/repositories/album.repository.ts | 8 +- .../repositories/shared-link.repository.ts | 2 +- server/src/services/album.service.spec.ts | 809 ++++++------------ server/test/factories/album-user.factory.ts | 54 ++ server/test/factories/album.factory.ts | 87 ++ server/test/factories/asset-exif.factory.ts | 55 ++ server/test/factories/asset.factory.ts | 79 ++ server/test/factories/auth.factory.ts | 48 ++ server/test/factories/builder.factory.ts | 5 + server/test/factories/shared-link.factory.ts | 63 ++ server/test/factories/types.ts | 16 + server/test/factories/user.factory.ts | 46 + 13 files changed, 727 insertions(+), 551 deletions(-) create mode 100644 server/test/factories/album-user.factory.ts create mode 100644 server/test/factories/album.factory.ts create mode 100644 server/test/factories/asset-exif.factory.ts create mode 100644 server/test/factories/asset.factory.ts create mode 100644 server/test/factories/auth.factory.ts create mode 100644 server/test/factories/builder.factory.ts create mode 100644 server/test/factories/shared-link.factory.ts create mode 100644 server/test/factories/types.ts create mode 100644 server/test/factories/user.factory.ts diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index f62e769a17..e3d7436c30 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -58,7 +58,7 @@ select from ( select - * + "shared_link".* from "shared_link" where @@ -243,7 +243,7 @@ select from ( select - * + "shared_link".* from "shared_link" where @@ -316,7 +316,7 @@ select from ( select - * + "shared_link".* from "shared_link" where diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 100ab908c0..cf132a023d 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -44,9 +44,9 @@ const withAlbumUsers = (eb: ExpressionBuilder) => { }; const withSharedLink = (eb: ExpressionBuilder) => { - return jsonArrayFrom(eb.selectFrom('shared_link').selectAll().whereRef('shared_link.albumId', '=', 'album.id')).as( - 'sharedLinks', - ); + return jsonArrayFrom( + eb.selectFrom('shared_link').selectAll('shared_link').whereRef('shared_link.albumId', '=', 'album.id'), + ).as('sharedLinks'); }; const withAssets = (eb: ExpressionBuilder) => { @@ -283,7 +283,7 @@ export class AlbumRepository { return tx .selectFrom('album') - .selectAll() + .selectAll('album') .where('id', '=', newAlbum.id) .select(withOwner) .select(withAssets) diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts index 8fab087156..37a5bca718 100644 --- a/server/src/repositories/shared-link.repository.ts +++ b/server/src/repositories/shared-link.repository.ts @@ -260,7 +260,7 @@ export class SharedLinkRepository { .selectAll('asset') .innerJoinLateral( (eb) => - eb.selectFrom('asset_exif').whereRef('asset_exif.assetId', '=', 'asset.id').selectAll().as('exif'), + eb.selectFrom('asset_exif').whereRef('asset_exif.assetId', '=', 'asset.id').selectAll().as('exifInfo'), (join) => join.onTrue(), ) .as('assets'), diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 209716db3a..d21185bd35 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -3,8 +3,13 @@ import _ from 'lodash'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { AlbumUserRole, AssetOrder, UserMetadataKey } from 'src/enum'; import { AlbumService } from 'src/services/album.service'; +import { AlbumUserFactory } from 'test/factories/album-user.factory'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { AuthFactory } from 'test/factories/auth.factory'; +import { UserFactory } from 'test/factories/user.factory'; import { authStub } from 'test/fixtures/auth.stub'; -import { factory } from 'test/small.factory'; +import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; describe(AlbumService.name, () => { @@ -38,13 +43,8 @@ describe(AlbumService.name, () => { describe('getAll', () => { it('gets list of albums for auth user', async () => { - const owner = factory.userAdmin(); - const album = { ...factory.album({ ownerId: owner.id }), owner }; - const sharedWithUserAlbum = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user: factory.user(), role: AlbumUserRole.Editor }], - }; + const album = AlbumFactory.from().albumUser().build(); + const sharedWithUserAlbum = AlbumFactory.from().owner(album.owner).albumUser().build(); mocks.album.getOwned.mockResolvedValue([album, sharedWithUserAlbum]); mocks.album.getMetadataForIds.mockResolvedValue([ { @@ -63,16 +63,14 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(factory.auth({ user: owner }), {}); + const result = await sut.getAll(AuthFactory.create(album.owner), {}); expect(result).toHaveLength(2); expect(result[0].id).toEqual(album.id); expect(result[1].id).toEqual(sharedWithUserAlbum.id); }); it('gets list of albums that have a specific asset', async () => { - const owner = factory.userAdmin(); - const asset = factory.asset({ ownerId: owner.id }); - const album = { ...factory.album({ ownerId: owner.id }), owner, assets: [asset] }; + const album = AlbumFactory.from().owner({ isAdmin: true }).albumUser().asset().asset().build(); mocks.album.getByAssetId.mockResolvedValue([album]); mocks.album.getMetadataForIds.mockResolvedValue([ { @@ -84,19 +82,14 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(factory.auth({ user: owner }), { assetId: asset.id }); + const result = await sut.getAll(AuthFactory.create(album.owner), { assetId: album.assets[0].id }); expect(result).toHaveLength(1); expect(result[0].id).toEqual(album.id); expect(mocks.album.getByAssetId).toHaveBeenCalledTimes(1); }); it('gets list of albums that are shared', async () => { - const owner = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user: factory.user(), role: AlbumUserRole.Editor }], - }; + const album = AlbumFactory.from().albumUser().build(); mocks.album.getShared.mockResolvedValue([album]); mocks.album.getMetadataForIds.mockResolvedValue([ { @@ -108,15 +101,14 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(factory.auth({ user: owner }), { shared: true }); + const result = await sut.getAll(AuthFactory.create(album.owner), { shared: true }); expect(result).toHaveLength(1); expect(result[0].id).toEqual(album.id); expect(mocks.album.getShared).toHaveBeenCalledTimes(1); }); it('gets list of albums that are NOT shared', async () => { - const owner = factory.userAdmin(); - const album = { ...factory.album({ ownerId: owner.id }), owner }; + const album = AlbumFactory.create(); mocks.album.getNotShared.mockResolvedValue([album]); mocks.album.getMetadataForIds.mockResolvedValue([ { @@ -128,7 +120,7 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(factory.auth({ user: owner }), { shared: false }); + const result = await sut.getAll(AuthFactory.create(album.owner), { shared: false }); expect(result).toHaveLength(1); expect(result[0].id).toEqual(album.id); expect(mocks.album.getNotShared).toHaveBeenCalledTimes(1); @@ -136,9 +128,7 @@ describe(AlbumService.name, () => { }); it('counts assets correctly', async () => { - const owner = factory.userAdmin(); - const asset = factory.asset({ ownerId: owner.id }); - const album = { ...factory.album({ ownerId: owner.id }), owner, assets: [asset] }; + const album = AlbumFactory.create(); mocks.album.getOwned.mockResolvedValue([album]); mocks.album.getMetadataForIds.mockResolvedValue([ { @@ -150,8 +140,7 @@ describe(AlbumService.name, () => { }, ]); - const result = await sut.getAll(factory.auth({ user: owner }), {}); - + const result = await sut.getAll(AuthFactory.create(album.owner), {}); expect(result).toHaveLength(1); expect(result[0].assetCount).toEqual(1); expect(mocks.album.getOwned).toHaveBeenCalledTimes(1); @@ -159,60 +148,52 @@ describe(AlbumService.name, () => { describe('create', () => { it('creates album', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const asset = { ...factory.asset({ ownerId: owner.id }), exifInfo: factory.exif() }; - const album = { - ...factory.album({ ownerId: owner.id, albumName: 'Empty album' }), - owner, - assets: [asset], - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; - mocks.album.create.mockResolvedValue(album); - mocks.user.get.mockResolvedValue(user); - mocks.user.getMetadata.mockResolvedValue([]); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + const assetId = newUuid(); + const albumUser = { userId: newUuid(), role: AlbumUserRole.Editor }; + const album = AlbumFactory.from({ albumName: 'test', description: 'description' }) + .asset({ id: assetId }, (asset) => asset.exif()) + .albumUser(albumUser) + .build(); - await sut.create(factory.auth({ user: owner }), { - albumName: 'Empty album', - albumUsers: [{ userId: user.id, role: AlbumUserRole.Editor }], - description: 'Album description', - assetIds: [asset.id], + mocks.album.create.mockResolvedValue(album); + mocks.user.get.mockResolvedValue(UserFactory.create(album.albumUsers[0].user)); + mocks.user.getMetadata.mockResolvedValue([]); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); + + await sut.create(AuthFactory.create(album.owner), { + albumName: 'test', + albumUsers: [albumUser], + description: 'description', + assetIds: [assetId], }); expect(mocks.album.create).toHaveBeenCalledWith( { - ownerId: owner.id, - albumName: album.albumName, - description: album.description, + ownerId: album.owner.id, + albumName: 'test', + description: 'description', order: album.order, - albumThumbnailAssetId: asset.id, + albumThumbnailAssetId: assetId, }, - [asset.id], - [{ userId: user.id, role: AlbumUserRole.Editor }], + [assetId], + [{ userId: albumUser.userId, role: AlbumUserRole.Editor }], ); - expect(mocks.user.get).toHaveBeenCalledWith(user.id, {}); - expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id]), false); - expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { - id: album.id, - userId: user.id, - }); + expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {}); + expect(mocks.user.getMetadata).toHaveBeenCalledWith(album.owner.id); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([assetId]), false); + expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { id: album.id, userId: albumUser.userId }); }); it('creates album with assetOrder from user preferences', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const asset = { ...factory.asset({ ownerId: owner.id }), exifInfo: factory.exif() }; - const album = { - ...factory.album({ ownerId: owner.id, albumName: 'Empty album' }), - owner, - assets: [asset], - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const assetId = newUuid(); + const albumUser = { userId: newUuid(), role: AlbumUserRole.Editor }; + const album = AlbumFactory.from() + .asset({ id: assetId }, (asset) => asset.exif()) + .albumUser(albumUser) + .build(); mocks.album.create.mockResolvedValue(album); - mocks.user.get.mockResolvedValue(user); + mocks.user.get.mockResolvedValue(album.albumUsers[0].user); mocks.user.getMetadata.mockResolvedValue([ { key: UserMetadataKey.Preferences, @@ -223,40 +204,37 @@ describe(AlbumService.name, () => { }, }, ]); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); - await sut.create(factory.auth({ user: owner }), { - albumName: 'Empty album', - albumUsers: [{ userId: user.id, role: AlbumUserRole.Editor }], - description: 'Album description', - assetIds: [asset.id], + await sut.create(AuthFactory.create(album.owner), { + albumName: album.albumName, + albumUsers: [albumUser], + description: album.description, + assetIds: [assetId], }); expect(mocks.album.create).toHaveBeenCalledWith( { - ownerId: owner.id, + ownerId: album.owner.id, albumName: album.albumName, description: album.description, order: 'asc', - albumThumbnailAssetId: asset.id, + albumThumbnailAssetId: assetId, }, - [asset.id], - [{ userId: user.id, role: AlbumUserRole.Editor }], + [assetId], + [albumUser], ); - expect(mocks.user.get).toHaveBeenCalledWith(user.id, {}); - expect(mocks.user.getMetadata).toHaveBeenCalledWith(owner.id); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id]), false); - expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { - id: album.id, - userId: user.id, - }); + expect(mocks.user.get).toHaveBeenCalledWith(albumUser.userId, {}); + expect(mocks.user.getMetadata).toHaveBeenCalledWith(album.owner.id); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([assetId]), false); + expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', { id: album.id, userId: albumUser.userId }); }); it('should require valid userIds', async () => { mocks.user.get.mockResolvedValue(void 0); await expect( - sut.create(factory.auth(), { + sut.create(AuthFactory.create(), { albumName: 'Empty album', albumUsers: [{ userId: 'unknown-user', role: AlbumUserRole.Editor }], }), @@ -266,47 +244,47 @@ describe(AlbumService.name, () => { }); it('should only add assets the user is allowed to access', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const asset = { ...factory.asset({ ownerId: owner.id }), exifInfo: factory.exif() }; - const album = { - ...factory.album({ ownerId: owner.id, albumName: 'Test album' }), - owner, - assets: [asset], - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; - mocks.user.get.mockResolvedValue(user); + const assetId = newUuid(); + const album = AlbumFactory.from() + .asset({ id: assetId }, (asset) => asset.exif()) + .albumUser() + .build(); + mocks.user.get.mockResolvedValue(album.albumUsers[0].user); mocks.album.create.mockResolvedValue(album); mocks.user.getMetadata.mockResolvedValue([]); - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([assetId])); - await sut.create(factory.auth({ user: owner }), { - albumName: 'Test album', - description: 'Album description', - assetIds: [asset.id, 'asset-2'], + await sut.create(AuthFactory.create(album.owner), { + albumName: album.albumName, + description: album.description, + assetIds: [assetId, 'asset-2'], }); expect(mocks.album.create).toHaveBeenCalledWith( { - ownerId: owner.id, + ownerId: album.owner.id, albumName: album.albumName, description: album.description, order: 'desc', - albumThumbnailAssetId: asset.id, + albumThumbnailAssetId: assetId, }, - [asset.id], + [assetId], [], ); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id, 'asset-2']), false); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( + album.owner.id, + new Set([assetId, 'asset-2']), + false, + ); }); it('should throw an error if the userId is the ownerId', async () => { - const owner = factory.userAdmin(); - mocks.user.get.mockResolvedValue(owner); + const album = AlbumFactory.create(); + mocks.user.get.mockResolvedValue(album.owner); await expect( - sut.create(factory.auth({ user: owner }), { + sut.create(AuthFactory.create(album.owner), { albumName: 'Empty album', - albumUsers: [{ userId: owner.id, role: AlbumUserRole.Editor }], + albumUsers: [{ userId: album.owner.id, role: AlbumUserRole.Editor }], }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.create).not.toHaveBeenCalled(); @@ -319,7 +297,7 @@ describe(AlbumService.name, () => { mocks.album.getById.mockResolvedValue(void 0); await expect( - sut.update(factory.auth(), 'invalid-id', { + sut.update(AuthFactory.create(), 'invalid-id', { albumName: 'Album', }), ).rejects.toBeInstanceOf(BadRequestException); @@ -328,28 +306,21 @@ describe(AlbumService.name, () => { }); it('should prevent updating a not owned album (shared with auth user)', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { ...factory.album({ ownerId: user.id }), user }; + const album = AlbumFactory.from().albumUser().build(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set()); await expect( - sut.update(factory.auth({ user: owner }), album.id, { - albumName: 'new album name', - }), + sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' }), ).rejects.toBeInstanceOf(BadRequestException); }); it('should require a valid thumbnail asset id', async () => { - const owner = factory.userAdmin(); - const album = { ...factory.album({ ownerId: owner.id }), owner }; + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValue(new Set()); await expect( - sut.update(factory.auth({ user: owner }), album.id, { - albumThumbnailAssetId: 'not-in-album', - }), + sut.update(AuthFactory.create(album.owner), album.id, { albumThumbnailAssetId: 'not-in-album' }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.getAssetIds).toHaveBeenCalledWith(album.id, ['not-in-album']); @@ -357,54 +328,44 @@ describe(AlbumService.name, () => { }); it('should allow the owner to update the album', async () => { - const owner = factory.userAdmin(); - const album = { ...factory.album({ ownerId: owner.id }), owner }; + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); - mocks.album.getById.mockResolvedValue(album); mocks.album.update.mockResolvedValue(album); - await sut.update(factory.auth({ user: owner }), album.id, { - albumName: 'new album name', - }); + await sut.update(AuthFactory.create(album.owner), album.id, { albumName: 'new album name' }); expect(mocks.album.update).toHaveBeenCalledTimes(1); - expect(mocks.album.update).toHaveBeenCalledWith(album.id, { - id: album.id, - albumName: 'new album name', - }); + expect(mocks.album.update).toHaveBeenCalledWith(album.id, { id: album.id, albumName: 'new album name' }); }); }); describe('delete', () => { it('should require permissions', async () => { - const album = factory.album(); + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set()); - await expect(sut.delete(factory.auth(), album.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.delete).not.toHaveBeenCalled(); }); it('should not let a shared user delete the album', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { ...factory.album({ ownerId: user.id }), owner: user }; + const album = AlbumFactory.create(); mocks.album.getById.mockResolvedValue(album); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set()); - await expect(sut.delete(factory.auth({ user: owner }), album.id)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.delete(AuthFactory.create(album.owner), album.id)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.delete).not.toHaveBeenCalled(); }); it('should let the owner delete an album', async () => { - const owner = factory.userAdmin(); - const album = { ...factory.album({ ownerId: owner.id }), owner }; + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); - await sut.delete(factory.auth({ user: owner }), album.id); + await sut.delete(AuthFactory.create(album.owner), album.id); expect(mocks.album.delete).toHaveBeenCalledTimes(1); expect(mocks.album.delete).toHaveBeenCalledWith(album.id); @@ -413,60 +374,46 @@ describe(AlbumService.name, () => { describe('addUsers', () => { it('should throw an error if the auth user is not the owner', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { ...factory.album({ ownerId: owner.id }), owner }; + const album = AlbumFactory.create(); + const user = UserFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set()); await expect( - sut.addUsers(factory.auth({ user }), album.id, { albumUsers: [{ userId: owner.id }] }), + sut.addUsers(AuthFactory.create(user), album.id, { albumUsers: [{ userId: newUuid() }] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should throw an error if the userId is already added', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const userId = newUuid(); + const album = AlbumFactory.from().albumUser({ userId }).build(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); await expect( - sut.addUsers(factory.auth({ user: owner }), album.id, { albumUsers: [{ userId: user.id }] }), + sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId }] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); expect(mocks.user.get).not.toHaveBeenCalled(); }); it('should throw an error if the userId does not exist', async () => { - const owner = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); mocks.user.get.mockResolvedValue(void 0); await expect( - sut.addUsers(factory.auth({ user: owner }), album.id, { albumUsers: [{ userId: 'unknown-user' }] }), + sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: 'unknown-user' }] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); expect(mocks.user.get).toHaveBeenCalledWith('unknown-user', {}); }); it('should throw an error if the userId is the ownerId', async () => { - const owner = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); await expect( - sut.addUsers(factory.auth({ user: owner }), album.id, { - albumUsers: [{ userId: owner.id }], + sut.addUsers(AuthFactory.create(album.owner), album.id, { + albumUsers: [{ userId: album.owner.id }], }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); @@ -474,24 +421,16 @@ describe(AlbumService.name, () => { }); it('should add valid shared users', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; + const album = AlbumFactory.create(); + const user = UserFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.update.mockResolvedValue(album); mocks.user.get.mockResolvedValue(user); - mocks.albumUser.create.mockResolvedValue({ - userId: user.id, - albumId: album.id, - role: AlbumUserRole.Editor, - }); - await sut.addUsers(factory.auth({ user: owner }), album.id, { - albumUsers: [{ userId: user.id }], - }); + mocks.albumUser.create.mockResolvedValue(AlbumUserFactory.from().album(album).user(user).build()); + + await sut.addUsers(AuthFactory.create(album.owner), album.id, { albumUsers: [{ userId: user.id }] }); + expect(mocks.albumUser.create).toHaveBeenCalledWith({ userId: user.id, albumId: album.id, @@ -507,105 +446,69 @@ describe(AlbumService.name, () => { it('should require a valid album id', async () => { mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); mocks.album.getById.mockResolvedValue(void 0); - await expect(sut.removeUser(factory.auth(), 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.removeUser(AuthFactory.create(), 'album-1', 'user-1')).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should remove a shared user from an owned album', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const userId = newUuid(); + const album = AlbumFactory.from().albumUser({ userId }).build(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); mocks.albumUser.delete.mockResolvedValue(); - await expect(sut.removeUser(factory.auth({ user: owner }), album.id, user.id)).resolves.toBeUndefined(); + await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, userId)).resolves.toBeUndefined(); expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1); - expect(mocks.albumUser.delete).toHaveBeenCalledWith({ - albumId: album.id, - userId: user.id, - }); + expect(mocks.albumUser.delete).toHaveBeenCalledWith({ albumId: album.id, userId }); expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: false }); }); it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const user2 = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: user.id }), - owner: user, - albumUsers: [ - { user: owner, role: AlbumUserRole.Editor }, - { user: user2, role: AlbumUserRole.Editor }, - ], - }; + const user1 = UserFactory.create(); + const user2 = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user1.id }).albumUser({ userId: user2.id }).build(); mocks.album.getById.mockResolvedValue(album); - await expect(sut.removeUser(factory.auth({ user: owner }), album.id, user2.id)).rejects.toBeInstanceOf( + await expect(sut.removeUser(AuthFactory.create(user1), album.id, user2.id)).rejects.toBeInstanceOf( BadRequestException, ); expect(mocks.albumUser.delete).not.toHaveBeenCalled(); - expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([album.id])); + expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(user1.id, new Set([album.id])); }); it('should allow a shared user to remove themselves', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: user.id }), - owner: user, - albumUsers: [{ user: owner, role: AlbumUserRole.Editor }], - }; + const user1 = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user1.id }).build(); mocks.album.getById.mockResolvedValue(album); mocks.albumUser.delete.mockResolvedValue(); - await sut.removeUser(factory.auth({ user: owner }), album.id, owner.id); + await sut.removeUser(AuthFactory.create(user1), album.id, user1.id); expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1); - expect(mocks.albumUser.delete).toHaveBeenCalledWith({ - albumId: album.id, - userId: owner.id, - }); + expect(mocks.albumUser.delete).toHaveBeenCalledWith({ albumId: album.id, userId: user1.id }); }); it('should allow a shared user to remove themselves using "me"', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); mocks.album.getById.mockResolvedValue(album); mocks.albumUser.delete.mockResolvedValue(); - await sut.removeUser(factory.auth({ user }), album.id, 'me'); + await sut.removeUser(AuthFactory.create(user), album.id, 'me'); expect(mocks.albumUser.delete).toHaveBeenCalledTimes(1); - expect(mocks.albumUser.delete).toHaveBeenCalledWith({ - albumId: album.id, - userId: user.id, - }); + expect(mocks.albumUser.delete).toHaveBeenCalledWith({ albumId: album.id, userId: user.id }); }); it('should not allow the owner to be removed', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const album = AlbumFactory.from().albumUser().build(); mocks.album.getById.mockResolvedValue(album); - await expect(sut.removeUser(factory.auth({ user: owner }), album.id, owner.id)).rejects.toBeInstanceOf( + await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, album.owner.id)).rejects.toBeInstanceOf( BadRequestException, ); @@ -613,16 +516,10 @@ describe(AlbumService.name, () => { }); it('should throw an error for a user not in the album', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const album = AlbumFactory.from().albumUser().build(); mocks.album.getById.mockResolvedValue(album); - await expect(sut.removeUser(factory.auth({ user: owner }), album.id, 'user-3')).rejects.toBeInstanceOf( + await expect(sut.removeUser(AuthFactory.create(album.owner), album.id, 'user-3')).rejects.toBeInstanceOf( BadRequestException, ); @@ -632,19 +529,13 @@ describe(AlbumService.name, () => { describe('updateUser', () => { it('should update user role', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.albumUser.update.mockResolvedValue(); - await sut.updateUser(factory.auth({ user: owner }), album.id, user.id, { - role: AlbumUserRole.Viewer, - }); + await sut.updateUser(AuthFactory.create(album.owner), album.id, user.id, { role: AlbumUserRole.Viewer }); + expect(mocks.albumUser.update).toHaveBeenCalledWith( { albumId: album.id, userId: user.id }, { role: AlbumUserRole.Viewer }, @@ -654,13 +545,7 @@ describe(AlbumService.name, () => { describe('getAlbumInfo', () => { it('should get a shared album', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const album = AlbumFactory.from().albumUser().build(); mocks.album.getById.mockResolvedValue(album); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getMetadataForIds.mockResolvedValue([ @@ -673,20 +558,14 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(factory.auth({ user: owner }), album.id, {}); + await sut.get(AuthFactory.create(album.owner), album.id, {}); expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); - expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([album.id])); + expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(album.owner.id, new Set([album.id])); }); it('should get a shared album via a shared link', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const album = AlbumFactory.from().albumUser().build(); mocks.album.getById.mockResolvedValue(album); mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id])); mocks.album.getMetadataForIds.mockResolvedValue([ @@ -699,7 +578,7 @@ describe(AlbumService.name, () => { }, ]); - const auth = factory.auth({ sharedLink: {} }); + const auth = AuthFactory.from().sharedLink().build(); await sut.get(auth, album.id, {}); expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); @@ -707,13 +586,8 @@ describe(AlbumService.name, () => { }); it('should get a shared album via shared with user', async () => { - const owner = factory.userAdmin(); - const user = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id }).build(); mocks.album.getById.mockResolvedValue(album); mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id])); mocks.album.getMetadataForIds.mockResolvedValue([ @@ -726,7 +600,7 @@ describe(AlbumService.name, () => { }, ]); - await sut.get(factory.auth({ user }), album.id, {}); + await sut.get(AuthFactory.create(user), album.id, {}); expect(mocks.album.getById).toHaveBeenCalledWith(album.id, { withAssets: true }); expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( @@ -737,7 +611,7 @@ describe(AlbumService.name, () => { }); it('should throw an error for no access', async () => { - const auth = factory.auth(); + const auth = AuthFactory.create(); await expect(sut.get(auth, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set(['album-123'])); @@ -751,19 +625,16 @@ describe(AlbumService.name, () => { describe('addAssets', () => { it('should allow the owner to add assets', async () => { - const owner = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const owner = UserFactory.create({ isAdmin: true }); + const album = AlbumFactory.from({ ownerId: owner.id }).owner(owner).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); await expect( - sut.addAssets(factory.auth({ user: owner }), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), + sut.addAssets(AuthFactory.create(owner), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), ).resolves.toEqual([ { success: true, id: asset1.id }, { success: true, id: asset2.id }, @@ -779,19 +650,14 @@ describe(AlbumService.name, () => { }); it('should not set the thumbnail if the album has one already', async () => { - const owner = factory.userAdmin(); - const [asset1, asset2] = [factory.asset(), factory.asset()]; - const album = { - ...factory.album({ ownerId: owner.id, albumThumbnailAssetId: asset1.id }), - owner, - assets: [{ ...asset1, exifInfo: factory.exif() }], - }; + const [asset1, asset2] = [AssetFactory.create(), AssetFactory.create()]; + const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset2.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - await expect(sut.addAssets(factory.auth({ user: owner }), album.id, { ids: [asset2.id] })).resolves.toEqual([ + await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset2.id] })).resolves.toEqual([ { success: true, id: asset2.id }, ]); @@ -804,21 +670,16 @@ describe(AlbumService.name, () => { }); it('should allow a shared user to add assets', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([album.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); await expect( - sut.addAssets(factory.auth({ user }), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), + sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), ).resolves.toEqual([ { success: true, id: asset1.id }, { success: true, id: asset2.id }, @@ -833,37 +694,28 @@ describe(AlbumService.name, () => { expect(mocks.album.addAssetIds).toHaveBeenCalledWith(album.id, [asset1.id, asset2.id, asset3.id]); expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { id: album.id, - recipientId: owner.id, + recipientId: album.ownerId, }); }); it('should not allow a shared user with viewer access to add assets', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Viewer }], - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const user = UserFactory.create(); + const album = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set()); mocks.album.getById.mockResolvedValue(album); await expect( - sut.addAssets(factory.auth({ user }), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), + sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset1.id, asset2.id, asset3.id] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.album.update).not.toHaveBeenCalled(); }); it('should allow a shared link user to add assets', async () => { - const owner = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; - const auth = factory.auth({ sharedLink: { allowUpload: true } }); + const album = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + const auth = AuthFactory.from(album.owner).sharedLink({ allowUpload: true, userId: album.ownerId }).build(); mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([album.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getById.mockResolvedValue(album); @@ -886,19 +738,14 @@ describe(AlbumService.name, () => { }); it('should allow adding assets shared via partner sharing', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const asset = factory.asset({ ownerId: user.id }); + const album = AlbumFactory.create(); + const asset = AssetFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - await expect(sut.addAssets(factory.auth({ user: owner }), album.id, { ids: [asset.id] })).resolves.toEqual([ + await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ { success: true, id: asset.id }, ]); @@ -907,23 +754,18 @@ describe(AlbumService.name, () => { updatedAt: expect.any(Date), albumThumbnailAssetId: asset.id, }); - expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id])); + expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id])); }); it('should skip duplicate assets', async () => { - const owner = factory.userAdmin(); - const asset = factory.asset(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - assets: [{ ...asset, exifInfo: factory.exif() }], - }; + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set([asset.id])); - await expect(sut.addAssets(factory.auth({ user: owner }), album.id, { ids: [asset.id] })).resolves.toEqual([ + await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ { success: false, id: asset.id, error: BulkIdErrorReason.DUPLICATE }, ]); @@ -931,35 +773,27 @@ describe(AlbumService.name, () => { }); it('should skip assets not shared with user', async () => { - const owner = factory.userAdmin(); - const asset = factory.asset(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()); - await expect(sut.addAssets(factory.auth({ user: owner }), album.id, { ids: [asset.id] })).resolves.toEqual([ + await expect(sut.addAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ { success: false, id: asset.id, error: BulkIdErrorReason.NO_PERMISSION }, ]); - expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id]), false); - expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(owner.id, new Set([asset.id])); + expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id]), false); + expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(album.ownerId, new Set([asset.id])); }); it('should not allow unauthorized access to the album', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const asset = factory.asset({ ownerId: user.id }); + const user = UserFactory.create(); + const album = AlbumFactory.create(); + const asset = AssetFactory.create({ ownerId: user.id }); mocks.album.getById.mockResolvedValue(album); - await expect(sut.addAssets(factory.auth({ user }), album.id, { ids: [asset.id] })).rejects.toBeInstanceOf( + await expect(sut.addAssets(AuthFactory.create(user), album.id, { ids: [asset.id] })).rejects.toBeInstanceOf( BadRequestException, ); @@ -968,16 +802,12 @@ describe(AlbumService.name, () => { }); it('should not allow unauthorized shared link access to the album', async () => { - const owner = factory.userAdmin(); - const album = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const asset = factory.asset(); + const album = AlbumFactory.create(); + const asset = AssetFactory.create(); mocks.album.getById.mockResolvedValue(album); await expect( - sut.addAssets(factory.auth({ sharedLink: { allowUpload: true } }), album.id, { ids: [asset.id] }), + sut.addAssets(AuthFactory.from().sharedLink({ allowUpload: true }).build(), album.id, { ids: [asset.id] }), ).rejects.toBeInstanceOf(BadRequestException); expect(mocks.access.album.checkSharedLinkAccess).toHaveBeenCalled(); @@ -986,23 +816,16 @@ describe(AlbumService.name, () => { describe('addAssetsToAlbums', () => { it('should allow the owner to add assets', async () => { - const owner = factory.userAdmin(); - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(factory.auth({ user: owner }), { + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1030,26 +853,17 @@ describe(AlbumService.name, () => { }); it('should not set the thumbnail if the album has one already', async () => { - const owner = factory.userAdmin(); - const asset = factory.asset(); - const album1 = { - ...factory.album({ ownerId: owner.id, albumThumbnailAssetId: asset.id }), - owner, - albumAssets: [asset], - }; - const album2 = { - ...factory.album({ ownerId: owner.id, albumThumbnailAssetId: asset.id }), - owner, - albumAssets: [asset], - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const asset = AssetFactory.create(); + const album1 = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).build(); + const album2 = AlbumFactory.from({ albumThumbnailAssetId: asset.id }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(factory.auth({ user: owner }), { + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1077,26 +891,17 @@ describe(AlbumService.name, () => { }); it('should allow a shared user to add assets', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Editor }], - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const user = UserFactory.create(); + const album1 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build(); + const album2 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Editor }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(factory.auth({ user }), { + sut.addAssetsToAlbums(AuthFactory.create(user), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1123,35 +928,26 @@ describe(AlbumService.name, () => { ]); expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { id: album1.id, - recipientId: owner.id, + recipientId: album1.ownerId, }); expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', { id: album2.id, - recipientId: owner.id, + recipientId: album2.ownerId, }); }); it('should not allow a shared user with viewer access to add assets', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Viewer }], - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - albumUsers: [{ user, role: AlbumUserRole.Viewer }], - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const user = UserFactory.create(); + const album1 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build(); + const album2 = AlbumFactory.from().albumUser({ userId: user.id, role: AlbumUserRole.Viewer }).build(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkSharedAlbumAccess.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(factory.auth({ user }), { + sut.addAssetsToAlbums(AuthFactory.create(user), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1164,22 +960,15 @@ describe(AlbumService.name, () => { }); it('should not allow a shared link user to add assets to multiple albums', async () => { - const owner = factory.userAdmin(); - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.access.album.checkSharedLinkAccess.mockResolvedValueOnce(new Set([album1.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); - const auth = factory.auth({ user: owner, sharedLink: { allowUpload: true } }); + const auth = AuthFactory.from(album1.owner).sharedLink({ allowUpload: true }).build(); await expect( sut.addAssetsToAlbums(auth, { albumIds: [album1.id, album2.id], @@ -1205,20 +994,13 @@ describe(AlbumService.name, () => { }); it('should allow adding assets shared via partner sharing', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; + const user = UserFactory.create(); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); const [asset1, asset2, asset3] = [ - factory.asset({ ownerId: user.id }), - factory.asset({ ownerId: user.id }), - factory.asset({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), ]; mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); mocks.access.asset.checkPartnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); @@ -1226,7 +1008,7 @@ describe(AlbumService.name, () => { mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(factory.auth({ user: owner }), { + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1252,23 +1034,15 @@ describe(AlbumService.name, () => { { albumId: album2.id, assetId: asset3.id }, ]); expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith( - owner.id, + album1.ownerId, new Set([asset1.id, asset2.id, asset3.id]), ); }); it('should skip some duplicate assets', async () => { - const owner = factory.userAdmin(); - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - albumAssets: [asset1, asset2, asset3], - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValueOnce(new Set([album1.id, album2.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); mocks.album.getAssetIds @@ -1277,7 +1051,7 @@ describe(AlbumService.name, () => { mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); await expect( - sut.addAssetsToAlbums(factory.auth({ user: owner }), { + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1297,18 +1071,9 @@ describe(AlbumService.name, () => { }); it('should skip all duplicate assets', async () => { - const owner = factory.userAdmin(); - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - albumAssets: [asset1, asset2, asset3], - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - albumAssets: [asset1, asset2, asset3], - }; + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); mocks.access.album.checkOwnerAccess .mockResolvedValueOnce(new Set([album1.id])) .mockResolvedValueOnce(new Set([album2.id])); @@ -1317,7 +1082,7 @@ describe(AlbumService.name, () => { mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id, asset3.id])); await expect( - sut.addAssetsToAlbums(factory.auth({ user: owner }), { + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1331,20 +1096,13 @@ describe(AlbumService.name, () => { }); it('should skip assets not shared with user', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; + const user = UserFactory.create(); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); const [asset1, asset2, asset3] = [ - factory.asset({ ownerId: user.id }), - factory.asset({ ownerId: user.id }), - factory.asset({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), + AssetFactory.create({ ownerId: user.id }), ]; mocks.access.album.checkSharedAlbumAccess .mockResolvedValueOnce(new Set([album1.id])) @@ -1353,7 +1111,7 @@ describe(AlbumService.name, () => { mocks.album.getAssetIds.mockResolvedValueOnce(new Set()).mockResolvedValueOnce(new Set()); await expect( - sut.addAssetsToAlbums(factory.auth({ user: owner }), { + sut.addAssetsToAlbums(AuthFactory.create(album1.owner), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1365,32 +1123,25 @@ describe(AlbumService.name, () => { expect(mocks.album.update).not.toHaveBeenCalled(); expect(mocks.album.addAssetIds).not.toHaveBeenCalled(); expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith( - owner.id, + album1.ownerId, new Set([asset1.id, asset2.id, asset3.id]), false, ); expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith( - owner.id, + album1.ownerId, new Set([asset1.id, asset2.id, asset3.id]), ); }); it('should not allow unauthorized access to the albums', async () => { - const owner = factory.userAdmin(); - const user = factory.user(); - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const user = UserFactory.create(); + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); await expect( - sut.addAssetsToAlbums(factory.auth({ user }), { + sut.addAssetsToAlbums(AuthFactory.create(user), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1406,20 +1157,13 @@ describe(AlbumService.name, () => { }); it('should not allow unauthorized shared link access to the album', async () => { - const owner = factory.userAdmin(); - const album1 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const album2 = { - ...factory.album({ ownerId: owner.id }), - owner, - }; - const [asset1, asset2, asset3] = [factory.asset(), factory.asset(), factory.asset()]; + const album1 = AlbumFactory.create(); + const album2 = AlbumFactory.create(); + const [asset1, asset2, asset3] = [AssetFactory.create(), AssetFactory.create(), AssetFactory.create()]; mocks.album.getById.mockResolvedValueOnce(album1).mockResolvedValueOnce(album2); await expect( - sut.addAssetsToAlbums(factory.auth({ sharedLink: { allowUpload: true } }), { + sut.addAssetsToAlbums(AuthFactory.from().sharedLink({ allowUpload: true }).build(), { albumIds: [album1.id, album2.id], assetIds: [asset1.id, asset2.id, asset3.id], }), @@ -1434,19 +1178,14 @@ describe(AlbumService.name, () => { describe('removeAssets', () => { it('should allow the owner to remove assets', async () => { - const owner = factory.userAdmin(); - const asset = factory.asset(); - const album = { - ...factory.album({ ownerId: owner.id, albumThumbnailAssetId: asset.id }), - owner, - albumAssets: [asset, factory.asset()], - }; + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id])); - await expect(sut.removeAssets(factory.auth({ user: owner }), album.id, { ids: [asset.id] })).resolves.toEqual([ + await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ { success: true, id: asset.id }, ]); @@ -1454,19 +1193,13 @@ describe(AlbumService.name, () => { }); it('should skip assets not in the album', async () => { - const owner = factory.userAdmin(); - const asset = factory.asset(); - const albumAsset = factory.asset(); - const album = { - ...factory.album({ ownerId: owner.id, albumThumbnailAssetId: albumAsset.id }), - owner, - albumAssets: [albumAsset], - }; + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValue(new Set()); - await expect(sut.removeAssets(factory.auth({ user: owner }), album.id, { ids: [asset.id] })).resolves.toEqual([ + await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ { success: false, id: asset.id, error: BulkIdErrorReason.NOT_FOUND }, ]); @@ -1474,37 +1207,27 @@ describe(AlbumService.name, () => { }); it('should allow owner to remove all assets from the album', async () => { - const owner = factory.userAdmin(); - const asset = factory.asset(); - const album = { - ...factory.album({ ownerId: owner.id, albumThumbnailAssetId: asset.id }), - owner, - albumAssets: [asset], - }; + const asset = AssetFactory.create(); + const album = AlbumFactory.create(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValue(new Set([asset.id])); - await expect(sut.removeAssets(factory.auth({ user: owner }), album.id, { ids: [asset.id] })).resolves.toEqual([ + await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset.id] })).resolves.toEqual([ { success: true, id: asset.id }, ]); }); it('should reset the thumbnail if it is removed', async () => { - const owner = factory.userAdmin(); - const asset1 = factory.asset(); - const asset2 = factory.asset(); - const album = { - ...factory.album({ ownerId: owner.id, albumThumbnailAssetId: asset1.id }), - owner, - albumAssets: [asset1, asset2], - }; + const asset1 = AssetFactory.create(); + const asset2 = AssetFactory.create(); + const album = AlbumFactory.from({ albumThumbnailAssetId: asset1.id }).build(); mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset1.id])); mocks.album.getById.mockResolvedValue(album); mocks.album.getAssetIds.mockResolvedValue(new Set([asset1.id, asset2.id])); - await expect(sut.removeAssets(factory.auth({ user: owner }), album.id, { ids: [asset1.id] })).resolves.toEqual([ + await expect(sut.removeAssets(AuthFactory.create(album.owner), album.id, { ids: [asset1.id] })).resolves.toEqual([ { success: true, id: asset1.id }, ]); diff --git a/server/test/factories/album-user.factory.ts b/server/test/factories/album-user.factory.ts new file mode 100644 index 0000000000..6e2f8cb832 --- /dev/null +++ b/server/test/factories/album-user.factory.ts @@ -0,0 +1,54 @@ +import { Selectable } from 'kysely'; +import { AlbumUserRole } from 'src/enum'; +import { AlbumUserTable } from 'src/schema/tables/album-user.table'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { build } from 'test/factories/builder.factory'; +import { AlbumUserLike, FactoryBuilder, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AlbumUserFactory { + #user!: UserFactory; + + private constructor(private readonly value: Selectable) { + value.userId ??= newUuid(); + this.#user = UserFactory.from({ id: value.userId }); + } + + static create(dto: AlbumUserLike = {}) { + return AlbumUserFactory.from(dto).build(); + } + + static from(dto: AlbumUserLike = {}) { + return new AlbumUserFactory({ + albumId: newUuid(), + userId: newUuid(), + role: AlbumUserRole.Editor, + createId: newUuidV7(), + createdAt: newDate(), + updateId: newUuidV7(), + updatedAt: newDate(), + ...dto, + }); + } + + album(dto: AlbumUserLike = {}, builder?: FactoryBuilder) { + const album = build(AlbumFactory.from(dto), builder); + this.value.albumId = album.build().id; + return this; + } + + user(dto: UserLike = {}, builder?: FactoryBuilder) { + const user = build(UserFactory.from(dto), builder); + this.value.userId = user.build().id; + this.#user = user; + return this; + } + + build() { + return { + ...this.value, + user: this.#user.build(), + }; + } +} diff --git a/server/test/factories/album.factory.ts b/server/test/factories/album.factory.ts new file mode 100644 index 0000000000..f401cd343d --- /dev/null +++ b/server/test/factories/album.factory.ts @@ -0,0 +1,87 @@ +import { Selectable } from 'kysely'; +import { AssetOrder } from 'src/enum'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { AlbumUserFactory } from 'test/factories/album-user.factory'; +import { AssetFactory } from 'test/factories/asset.factory'; +import { build } from 'test/factories/builder.factory'; +import { AlbumLike, AlbumUserLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AlbumFactory { + #owner: UserFactory; + #sharedLinks: Selectable[] = []; + #albumUsers: AlbumUserFactory[] = []; + #assets: AssetFactory[] = []; + + private constructor(private readonly value: Selectable) { + value.ownerId ??= newUuid(); + this.#owner = UserFactory.from({ id: value.ownerId }); + } + + static create(dto: AlbumLike = {}) { + return AlbumFactory.from(dto).build(); + } + + static from(dto: AlbumLike = {}) { + return new AlbumFactory({ + id: newUuid(), + ownerId: newUuid(), + albumName: 'My Album', + albumThumbnailAssetId: null, + createdAt: newDate(), + deletedAt: null, + description: 'Album description', + isActivityEnabled: false, + order: AssetOrder.Desc, + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }).owner(); + } + + owner(dto: UserLike = {}, builder?: FactoryBuilder) { + this.#owner = build(UserFactory.from(dto), builder); + this.value.ownerId = this.#owner.build().id; + return this; + } + + sharedLinks() { + this.#sharedLinks = []; + return this; + } + + albumUser(dto: AlbumUserLike = {}, builder?: FactoryBuilder) { + const albumUser = build(AlbumUserFactory.from(dto).album(this.value), builder); + this.#albumUsers.push(albumUser); + return this; + } + + asset(dto: AssetLike = {}, builder?: FactoryBuilder) { + const asset = build(AssetFactory.from(dto), builder); + + // use album owner by default + if (!dto.ownerId) { + asset.owner(this.#owner.build()); + } + + if (!this.#assets) { + this.#assets = []; + } + + this.#assets.push(asset); + + return this; + } + + build() { + return { + ...this.value, + owner: this.#owner.build(), + assets: this.#assets.map((asset) => asset.build()), + albumUsers: this.#albumUsers.map((albumUser) => albumUser.build()), + sharedLinks: this.#sharedLinks, + }; + } +} diff --git a/server/test/factories/asset-exif.factory.ts b/server/test/factories/asset-exif.factory.ts new file mode 100644 index 0000000000..da4d689ebf --- /dev/null +++ b/server/test/factories/asset-exif.factory.ts @@ -0,0 +1,55 @@ +import { Selectable } from 'kysely'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; +import { AssetExifLike } from 'test/factories/types'; +import { factory } from 'test/small.factory'; + +export class AssetExifFactory { + private constructor(private readonly value: Selectable) {} + + static create(dto: AssetExifLike = {}) { + return AssetExifFactory.from(dto).build(); + } + + static from(dto: AssetExifLike = {}) { + return new AssetExifFactory({ + updatedAt: factory.date(), + updateId: factory.uuid(), + assetId: factory.uuid(), + autoStackId: null, + bitsPerSample: null, + city: 'Austin', + colorspace: null, + country: 'United States of America', + dateTimeOriginal: factory.date(), + 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: factory.date(), + orientation: '1', + profileDescription: null, + projectionType: null, + rating: 4, + lockedProperties: [], + state: 'Texas', + tags: ['parent/child'], + timeZone: 'UTC-6', + ...dto, + }); + } + + build() { + return { ...this.value }; + } +} diff --git a/server/test/factories/asset.factory.ts b/server/test/factories/asset.factory.ts new file mode 100644 index 0000000000..41714dbf67 --- /dev/null +++ b/server/test/factories/asset.factory.ts @@ -0,0 +1,79 @@ +import { Selectable } from 'kysely'; +import { AssetStatus, AssetType, AssetVisibility } from 'src/enum'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { AssetExifFactory } from 'test/factories/asset-exif.factory'; +import { build } from 'test/factories/builder.factory'; +import { AssetExifLike, AssetLike, FactoryBuilder, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; +import { factory, newDate, newSha1, newUuid, newUuidV7 } from 'test/small.factory'; + +export class AssetFactory { + #assetExif?: AssetExifFactory; + #owner!: UserFactory; + + private constructor(private readonly value: Selectable) { + value.ownerId ??= newUuid(); + this.#owner = UserFactory.from({ id: value.ownerId }); + } + + static create(dto: AssetLike = {}) { + return AssetFactory.from(dto).build(); + } + + static from(dto: AssetLike = {}) { + return new AssetFactory({ + id: factory.uuid(), + 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, + ...dto, + }); + } + + owner(dto: UserLike = {}, builder?: FactoryBuilder) { + this.#owner = build(UserFactory.from(dto), builder); + this.value.ownerId = this.#owner.build().id; + return this; + } + + exif(dto: AssetExifLike = {}, builder?: FactoryBuilder) { + this.#assetExif = build(AssetExifFactory.from(dto), builder); + return this; + } + + build() { + const exif = this.#assetExif?.build(); + + return { + ...this.value, + exifInfo: exif as NonNullable, + owner: this.#owner.build(), + }; + } +} diff --git a/server/test/factories/auth.factory.ts b/server/test/factories/auth.factory.ts new file mode 100644 index 0000000000..9c738aabac --- /dev/null +++ b/server/test/factories/auth.factory.ts @@ -0,0 +1,48 @@ +import { AuthDto } from 'src/dtos/auth.dto'; +import { build } from 'test/factories/builder.factory'; +import { SharedLinkFactory } from 'test/factories/shared-link.factory'; +import { FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; + +export class AuthFactory { + #user: UserFactory; + #sharedLink?: SharedLinkFactory; + + private constructor(user: UserFactory) { + this.#user = user; + } + + static create(dto: UserLike = {}) { + return AuthFactory.from(dto).build(); + } + + static from(dto: UserLike = {}) { + return new AuthFactory(UserFactory.from(dto)); + } + + apiKey() { + // TODO + return this; + } + + sharedLink(dto: SharedLinkLike = {}, builder?: FactoryBuilder) { + this.#sharedLink = build(SharedLinkFactory.from(dto), builder); + return this; + } + + build(): AuthDto { + const { id, isAdmin, name, email, quotaUsageInBytes, quotaSizeInBytes } = this.#user.build(); + + return { + user: { + id, + isAdmin, + name, + email, + quotaUsageInBytes, + quotaSizeInBytes, + }, + sharedLink: this.#sharedLink?.build(), + }; + } +} diff --git a/server/test/factories/builder.factory.ts b/server/test/factories/builder.factory.ts new file mode 100644 index 0000000000..4efa7a498f --- /dev/null +++ b/server/test/factories/builder.factory.ts @@ -0,0 +1,5 @@ +import { FactoryBuilder } from 'test/factories/types'; + +export const build = (factory: T, builder?: FactoryBuilder) => { + return builder ? builder(factory) : factory; +}; diff --git a/server/test/factories/shared-link.factory.ts b/server/test/factories/shared-link.factory.ts new file mode 100644 index 0000000000..585b43dd84 --- /dev/null +++ b/server/test/factories/shared-link.factory.ts @@ -0,0 +1,63 @@ +import { Selectable } from 'kysely'; +import { SharedLinkType } from 'src/enum'; +import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { AlbumFactory } from 'test/factories/album.factory'; +import { build } from 'test/factories/builder.factory'; +import { AlbumLike, FactoryBuilder, SharedLinkLike, UserLike } from 'test/factories/types'; +import { UserFactory } from 'test/factories/user.factory'; +import { factory, newDate, newUuid } from 'test/small.factory'; + +export class SharedLinkFactory { + #owner: UserFactory; + #album?: AlbumFactory; + + private constructor(private readonly value: Selectable) { + value.userId ??= newUuid(); + this.#owner = UserFactory.from({ id: value.userId }); + } + + static create(dto: SharedLinkLike = {}) { + return SharedLinkFactory.from(dto).build(); + } + + static from(dto: SharedLinkLike = {}) { + const type = dto.type ?? SharedLinkType.Individual; + const albumId = (dto.albumId ?? type === SharedLinkType.Album) ? newUuid() : null; + + return new SharedLinkFactory({ + id: factory.uuid(), + description: 'Shared link description', + userId: newUuid(), + key: factory.buffer(), + type, + albumId, + createdAt: newDate(), + expiresAt: null, + allowUpload: true, + allowDownload: true, + showExif: true, + password: null, + slug: null, + ...dto, + }); + } + + owner(dto: UserLike = {}, builder?: FactoryBuilder): SharedLinkFactory { + this.#owner = build(UserFactory.from(dto), builder); + return this; + } + + album(dto: AlbumLike = {}, builder?: FactoryBuilder) { + this.#album = build(AlbumFactory.from(dto), builder); + return this; + } + + build() { + return { + ...this.value, + owner: this.#owner.build(), + album: this.#album?.build(), + assets: [], + }; + } +} diff --git a/server/test/factories/types.ts b/server/test/factories/types.ts new file mode 100644 index 0000000000..43ae26c9f4 --- /dev/null +++ b/server/test/factories/types.ts @@ -0,0 +1,16 @@ +import { Selectable } from 'kysely'; +import { AlbumUserTable } from 'src/schema/tables/album-user.table'; +import { AlbumTable } from 'src/schema/tables/album.table'; +import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; +import { AssetTable } from 'src/schema/tables/asset.table'; +import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; +import { UserTable } from 'src/schema/tables/user.table'; + +export type FactoryBuilder = (builder: T) => R; + +export type AssetLike = Partial>; +export type AssetExifLike = Partial>; +export type AlbumLike = Partial>; +export type AlbumUserLike = Partial>; +export type SharedLinkLike = Partial>; +export type UserLike = Partial>; diff --git a/server/test/factories/user.factory.ts b/server/test/factories/user.factory.ts new file mode 100644 index 0000000000..e6e84d94a1 --- /dev/null +++ b/server/test/factories/user.factory.ts @@ -0,0 +1,46 @@ +import { Selectable } from 'kysely'; +import { UserStatus } from 'src/enum'; +import { UserMetadataTable } from 'src/schema/tables/user-metadata.table'; +import { UserTable } from 'src/schema/tables/user.table'; +import { UserLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class UserFactory { + private constructor(private value: Selectable) {} + + static create(dto: UserLike = {}) { + return UserFactory.from(dto).build(); + } + + static from(dto: UserLike = {}) { + return new UserFactory({ + id: newUuid(), + email: 'test@immich.cloud', + password: '', + pinCode: null, + createdAt: newDate(), + profileImagePath: '', + isAdmin: false, + shouldChangePassword: false, + avatarColor: null, + deletedAt: null, + oauthId: '', + updatedAt: newDate(), + storageLabel: null, + name: 'Test User', + quotaSizeInBytes: null, + quotaUsageInBytes: 0, + status: UserStatus.Active, + profileChangedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + build() { + return { + ...this.value, + metadata: [] as UserMetadataTable[], + }; + } +}