From 5a9acbc05b8291cb33d56e877c9f84c85cd98fee Mon Sep 17 00:00:00 2001 From: Efren Date: Tue, 17 Oct 2023 00:38:42 -0700 Subject: [PATCH 01/24] Fix Issues hyperlink pointing to Releases (#4508) --- docs/docs/overview/help.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/overview/help.md b/docs/docs/overview/help.md index 62261cbe4..33a35b9b3 100644 --- a/docs/docs/overview/help.md +++ b/docs/docs/overview/help.md @@ -11,6 +11,6 @@ Running into an issue or have a question? Try the following: 3. Search through existing [GitHub Issues][github-issues]. 4. Open a help ticket on [Discord][discord-link]. -[github-issues]: https://github.com/immich-app/immich/releases +[github-issues]: https://github.com/immich-app/immich/issues [github-releases]: https://github.com/immich-app/immich/releases [discord-link]: https://discord.com/invite/D8JsnBEuKb From 335216f6ddc7f4881c3e114b9ebf0115ba0b5d82 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 17 Oct 2023 17:34:16 -0400 Subject: [PATCH 02/24] feat(server): allow underscores in ML url (#4517) --- .../dto/system-config-machine-learning.dto.ts | 2 +- .../domain/system-config/system-config.service.spec.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts b/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts index 330560ea8..38d977f88 100644 --- a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts +++ b/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts @@ -6,7 +6,7 @@ export class SystemConfigMachineLearningDto { @IsBoolean() enabled!: boolean; - @IsUrl({ require_tld: false }) + @IsUrl({ require_tld: false, allow_underscores: true }) @ValidateIf((dto) => dto.enabled) url!: string; diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index ecdec41fd..d47b0ea7c 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -189,6 +189,15 @@ describe(SystemConfigService.name, () => { expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json'); }); + it('should allow underscores in the machine learning url', async () => { + process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; + const partialConfig = { machineLearning: { url: 'immich_machine_learning' } }; + configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify(partialConfig))); + + const config = await sut.getConfig(); + expect(config.machineLearning.url).toEqual('immich_machine_learning'); + }); + const tests = [ { should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } }, { should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } }, From f4a12acd2993bcd579bf776bab94055af5ec2f15 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 18 Oct 2023 11:54:20 -0400 Subject: [PATCH 03/24] fix(web): scrollbar offset (#4518) * fix(web): scrollbar offset * fix offset on photo page * proper fix --------- Co-authored-by: Alex Tran --- .../layouts/user-page-layout.svelte | 28 ++++---- .../components/photos-page/asset-grid.svelte | 1 + web/src/routes/(user)/archive/+page.svelte | 2 +- web/src/routes/(user)/favorites/+page.svelte | 2 +- web/src/routes/(user)/photos/+page.svelte | 65 +++++++++---------- web/src/routes/(user)/trash/+page.svelte | 4 +- 6 files changed, 50 insertions(+), 52 deletions(-) diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 483090503..8f7212ae2 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -13,6 +13,7 @@ export let admin = false; $: scrollbarClass = scrollbar ? 'immich-scrollbar p-4 pb-8' : 'scrollbar-hidden pl-4'; + $: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full';
@@ -32,20 +33,19 @@ {/if} - - {#if title} -
-
-

{title}

- -
-
- -
-
+
+ {#if title} +
+

{title}

+ +
{/if} - + +
+ +
+
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 0fc271163..4fab2662a 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -25,6 +25,7 @@ export let assetStore: AssetStore; export let assetInteractionStore: AssetInteractionStore; export let removeAction: AssetAction | null = null; + $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash; export let forceDelete = false; diff --git a/web/src/routes/(user)/archive/+page.svelte b/web/src/routes/(user)/archive/+page.svelte index 7fee8c915..a046ca4ac 100644 --- a/web/src/routes/(user)/archive/+page.svelte +++ b/web/src/routes/(user)/archive/+page.svelte @@ -45,7 +45,7 @@ {/if} - + {/if} - + - - - {#if $isMultiSelectState} - assetInteractionStore.clearMultiselect()}> - (handleEscapeKey = true)} /> - - - - - - (handleEscapeKey = true)} - onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} - /> - - - - assetStore.removeAssets(ids)} /> - - - +{#if $isMultiSelectState} + assetInteractionStore.clearMultiselect()}> + (handleEscapeKey = true)} /> + + + + + + (handleEscapeKey = true)} + onAssetDelete={(assetId) => assetStore.removeAsset(assetId)} + /> + + + + assetStore.removeAssets(ids)} /> + + + +{/if} + + + + {#if data.user.memoriesEnabled} + {/if} - - - - {#if data.user.memoriesEnabled} - - {/if} - openFileUploadDialog()} - slot="empty" - /> - - + openFileUploadDialog()} + slot="empty" + /> + diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte index 0cb676b5c..cce565767 100644 --- a/web/src/routes/(user)/trash/+page.svelte +++ b/web/src/routes/(user)/trash/+page.svelte @@ -70,7 +70,7 @@ {/if} {#if $featureFlags.loaded && $featureFlags.trash} - +
@@ -87,7 +87,7 @@
-

+

Trashed items will be permanently deleted after {$serverConfig.trashDays} days.

Date: Wed, 18 Oct 2023 11:56:00 -0400 Subject: [PATCH 04/24] fix(server): album add/remove asset performance (#4516) --- server/src/domain/album/album.service.spec.ts | 46 ++++++++------ server/src/domain/album/album.service.ts | 60 +++++++++---------- .../domain/repositories/album.repository.ts | 17 +++++- .../infra/repositories/album.repository.ts | 30 ++++++++-- .../repositories/album.repository.mock.ts | 2 + 5 files changed, 97 insertions(+), 58 deletions(-) diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts index e326a2711..453539129 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/domain/album/album.service.spec.ts @@ -1,7 +1,6 @@ import { BadRequestException } from '@nestjs/common'; import { albumStub, - assetStub, authStub, IAccessRepositoryMock, newAccessRepositoryMock, @@ -225,7 +224,7 @@ describe(AlbumService.name, () => { }), ).rejects.toBeInstanceOf(BadRequestException); - expect(albumMock.hasAsset).toHaveBeenCalledWith(albumStub.oneAsset.id, 'not-in-album'); + expect(albumMock.hasAsset).toHaveBeenCalledWith({ albumId: 'album-4', assetId: 'not-in-album' }); expect(albumMock.update).not.toHaveBeenCalled(); }); @@ -461,6 +460,7 @@ describe(AlbumService.name, () => { accessMock.album.hasOwnerAccess.mockResolvedValue(true); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + albumMock.hasAsset.mockResolvedValue(false); await expect( sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), @@ -473,9 +473,12 @@ describe(AlbumService.name, () => { expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date), - assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }], albumThumbnailAssetId: 'asset-1', }); + expect(albumMock.addAssets).toHaveBeenCalledWith({ + albumId: 'album-123', + assetIds: ['asset-1', 'asset-2', 'asset-3'], + }); }); it('should not set the thumbnail if the album has one already', async () => { @@ -490,9 +493,9 @@ describe(AlbumService.name, () => { expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date), - assets: [{ id: 'asset-1' }], albumThumbnailAssetId: 'asset-id', }); + expect(albumMock.addAssets).toHaveBeenCalled(); }); it('should allow a shared user to add assets', async () => { @@ -512,9 +515,12 @@ describe(AlbumService.name, () => { expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date), - assets: [{ id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }], albumThumbnailAssetId: 'asset-1', }); + expect(albumMock.addAssets).toHaveBeenCalledWith({ + albumId: 'album-123', + assetIds: ['asset-1', 'asset-2', 'asset-3'], + }); }); it('should allow a shared link user to add assets', async () => { @@ -523,6 +529,7 @@ describe(AlbumService.name, () => { accessMock.album.hasSharedLinkAccess.mockResolvedValue(true); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + albumMock.hasAsset.mockResolvedValue(false); await expect( sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), @@ -535,9 +542,12 @@ describe(AlbumService.name, () => { expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date), - assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }], albumThumbnailAssetId: 'asset-1', }); + expect(albumMock.addAssets).toHaveBeenCalledWith({ + albumId: 'album-123', + assetIds: ['asset-1', 'asset-2', 'asset-3'], + }); expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLinkId, @@ -550,6 +560,7 @@ describe(AlbumService.name, () => { accessMock.asset.hasOwnerAccess.mockResolvedValue(false); accessMock.asset.hasPartnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + albumMock.hasAsset.mockResolvedValue(false); await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([ { success: true, id: 'asset-1' }, @@ -558,10 +569,8 @@ describe(AlbumService.name, () => { expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date), - assets: [assetStub.image, { id: 'asset-1' }], albumThumbnailAssetId: 'asset-1', }); - expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1'); }); @@ -569,6 +578,7 @@ describe(AlbumService.name, () => { accessMock.album.hasOwnerAccess.mockResolvedValue(true); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + albumMock.hasAsset.mockResolvedValue(true); await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ { success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE }, @@ -620,17 +630,14 @@ describe(AlbumService.name, () => { it('should allow the owner to remove assets', async () => { accessMock.album.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + albumMock.hasAsset.mockResolvedValue(true); await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ { success: true, id: 'asset-id' }, ]); - expect(albumMock.update).toHaveBeenCalledWith({ - id: 'album-123', - updatedAt: expect.any(Date), - assets: [], - albumThumbnailAssetId: null, - }); + expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) }); + expect(albumMock.removeAssets).toHaveBeenCalledWith({ assetIds: ['asset-id'], albumId: 'album-123' }); }); it('should skip assets not in the album', async () => { @@ -647,9 +654,14 @@ describe(AlbumService.name, () => { it('should skip assets without user permission to remove', async () => { accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); + albumMock.hasAsset.mockResolvedValue(true); await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ - { success: false, id: 'asset-id', error: BulkIdErrorReason.NO_PERMISSION }, + { + success: false, + id: 'asset-id', + error: BulkIdErrorReason.NO_PERMISSION, + }, ]); expect(albumMock.update).not.toHaveBeenCalled(); @@ -658,6 +670,7 @@ describe(AlbumService.name, () => { it('should reset the thumbnail if it is removed', async () => { accessMock.album.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets)); + albumMock.hasAsset.mockResolvedValue(true); await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([ { success: true, id: 'asset-id' }, @@ -666,9 +679,8 @@ describe(AlbumService.name, () => { expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date), - assets: [assetStub.withLocation], - albumThumbnailAssetId: assetStub.withLocation.id, }); + expect(albumMock.updateThumbnails).toHaveBeenCalled(); }); }); diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 5da0b3440..04b885040 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -120,7 +120,7 @@ export class AlbumService { const album = await this.findOrFail(id, { withAssets: true }); if (dto.albumThumbnailAssetId) { - const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId); + const valid = await this.albumRepository.hasAsset({ albumId: id, assetId: dto.albumThumbnailAssetId }); if (!valid) { throw new BadRequestException('Invalid album thumbnail'); } @@ -148,35 +148,34 @@ export class AlbumService { } async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise { - const album = await this.findOrFail(id, { withAssets: true }); + const album = await this.findOrFail(id, { withAssets: false }); await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); const results: BulkIdResponseDto[] = []; - for (const id of dto.ids) { - const hasAsset = album.assets.find((asset) => asset.id === id); + for (const assetId of dto.ids) { + const hasAsset = await this.albumRepository.hasAsset({ albumId: id, assetId }); if (hasAsset) { - results.push({ id, success: false, error: BulkIdErrorReason.DUPLICATE }); + results.push({ id: assetId, success: false, error: BulkIdErrorReason.DUPLICATE }); continue; } - const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, id); + const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId); if (!hasAccess) { - results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION }); + results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; } - results.push({ id, success: true }); - album.assets.push({ id } as AssetEntity); + results.push({ id: assetId, success: true }); } - const newAsset = results.find(({ success }) => success); - if (newAsset) { + const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id); + if (newAssetIds.length > 0) { + await this.albumRepository.addAssets({ albumId: id, assetIds: newAssetIds }); await this.albumRepository.update({ id, - assets: album.assets, updatedAt: new Date(), - albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAsset.id, + albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAssetIds[0], }); } @@ -184,42 +183,37 @@ export class AlbumService { } async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise { - const album = await this.findOrFail(id, { withAssets: true }); + const album = await this.findOrFail(id, { withAssets: false }); await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); const results: BulkIdResponseDto[] = []; - for (const id of dto.ids) { - const hasAsset = album.assets.find((asset) => asset.id === id); + for (const assetId of dto.ids) { + const hasAsset = await this.albumRepository.hasAsset({ albumId: id, assetId }); if (!hasAsset) { - results.push({ id, success: false, error: BulkIdErrorReason.NOT_FOUND }); + results.push({ id: assetId, success: false, error: BulkIdErrorReason.NOT_FOUND }); continue; } const hasAccess = await this.access.hasAny(authUser, [ - { permission: Permission.ALBUM_REMOVE_ASSET, id }, - { permission: Permission.ASSET_SHARE, id }, + { permission: Permission.ALBUM_REMOVE_ASSET, id: assetId }, + { permission: Permission.ASSET_SHARE, id: assetId }, ]); if (!hasAccess) { - results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION }); + results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; } - results.push({ id, success: true }); - album.assets = album.assets.filter((asset) => asset.id !== id); - if (album.albumThumbnailAssetId === id) { - album.albumThumbnailAssetId = null; - } + results.push({ id: assetId, success: true }); } - const hasSuccess = results.find(({ success }) => success); - if (hasSuccess) { - await this.albumRepository.update({ - id, - assets: album.assets, - updatedAt: new Date(), - albumThumbnailAssetId: album.albumThumbnailAssetId || album.assets[0]?.id || null, - }); + const removedIds = results.filter(({ success }) => success).map(({ id }) => id); + if (removedIds.length > 0) { + await this.albumRepository.removeAssets({ albumId: id, assetIds: removedIds }); + await this.albumRepository.update({ id, updatedAt: new Date() }); + if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) { + await this.albumRepository.updateThumbnails(); + } } return results; diff --git a/server/src/domain/repositories/album.repository.ts b/server/src/domain/repositories/album.repository.ts index 5a54dbc80..276ab796b 100644 --- a/server/src/domain/repositories/album.repository.ts +++ b/server/src/domain/repositories/album.repository.ts @@ -11,13 +11,24 @@ export interface AlbumInfoOptions { withAssets: boolean; } +export interface AlbumAsset { + albumId: string; + assetId: string; +} + +export interface AlbumAssets { + albumId: string; + assetIds: string[]; +} + export interface IAlbumRepository { getById(id: string, options: AlbumInfoOptions): Promise; getByIds(ids: string[]): Promise; getByAssetId(ownerId: string, assetId: string): Promise; - hasAsset(id: string, assetId: string): Promise; - /** Remove an asset from _all_ albums */ - removeAsset(id: string): Promise; + addAssets(assets: AlbumAssets): Promise; + hasAsset(asset: AlbumAsset): Promise; + removeAsset(assetId: string): Promise; + removeAssets(assets: AlbumAssets): Promise; getAssetCountForIds(ids: string[]): Promise; getInvalidThumbnail(): Promise; getOwned(ownerId: string): Promise; diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts index b53c93471..a8cd50414 100644 --- a/server/src/infra/repositories/album.repository.ts +++ b/server/src/infra/repositories/album.repository.ts @@ -1,4 +1,4 @@ -import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from '@app/domain'; +import { AlbumAsset, AlbumAssetCount, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain'; import { Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm'; @@ -168,16 +168,27 @@ export class AlbumRepository implements IAlbumRepository { .createQueryBuilder() .delete() .from('albums_assets_assets') - .where('"albums_assets_assets"."assetsId" = :assetId', { assetId }) + .where('"albums_assets_assets"."assetsId" = :assetId', { assetId }); + } + + async removeAssets(asset: AlbumAssets): Promise { + await this.dataSource + .createQueryBuilder() + .delete() + .from('albums_assets_assets') + .where({ + albumsId: asset.albumId, + assetsId: In(asset.assetIds), + }) .execute(); } - hasAsset(id: string, assetId: string): Promise { + hasAsset(asset: AlbumAsset): Promise { return this.repository.exist({ where: { - id, + id: asset.albumId, assets: { - id: assetId, + id: asset.assetId, }, }, relations: { @@ -186,6 +197,15 @@ export class AlbumRepository implements IAlbumRepository { }); } + async addAssets({ albumId, assetIds }: AlbumAssets): Promise { + await this.dataSource + .createQueryBuilder() + .insert() + .into('albums_assets_assets', ['albumsId', 'assetsId']) + .values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId }))) + .execute(); + } + async create(album: Partial): Promise { return this.save(album); } diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts index 25206f028..20c355269 100644 --- a/server/test/repositories/album.repository.mock.ts +++ b/server/test/repositories/album.repository.mock.ts @@ -14,7 +14,9 @@ export const newAlbumRepositoryMock = (): jest.Mocked => { softDeleteAll: jest.fn(), deleteAll: jest.fn(), getAll: jest.fn(), + addAssets: jest.fn(), removeAsset: jest.fn(), + removeAssets: jest.fn(), hasAsset: jest.fn(), create: jest.fn(), update: jest.fn(), From 23f0eb6fe8a0ceeec195e7023072724812f31d16 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 18 Oct 2023 12:12:19 -0500 Subject: [PATCH 05/24] fix(web): fix websocket mode (#4531) --- web/src/lib/stores/websocket.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index a166dfda1..af250984a 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -16,7 +16,6 @@ export const openWebsocketConnection = () => { try { const websocket = io('', { path: '/api/socket.io', - transports: ['polling'], reconnection: true, forceNew: true, autoConnect: true, From 31987bc04321949aaa584be95ba016975aa27237 Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Wed, 18 Oct 2023 17:14:26 +0000 Subject: [PATCH 06/24] Version v1.82.1 --- cli/src/api/open-api/api.ts | 2 +- cli/src/api/open-api/base.ts | 2 +- cli/src/api/open-api/common.ts | 2 +- cli/src/api/open-api/configuration.ts | 2 +- cli/src/api/open-api/index.ts | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 2 +- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- server/immich-openapi-specs.json | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/src/api/open-api/api.ts | 2 +- web/src/api/open-api/base.ts | 2 +- web/src/api/open-api/common.ts | 2 +- web/src/api/open-api/configuration.ts | 2 +- web/src/api/open-api/index.ts | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 03fb10f50..d9c34475a 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/base.ts b/cli/src/api/open-api/base.ts index 2abcb2b1d..84c4cbb35 100644 --- a/cli/src/api/open-api/base.ts +++ b/cli/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/common.ts b/cli/src/api/open-api/common.ts index 41329bb53..39307ae08 100644 --- a/cli/src/api/open-api/common.ts +++ b/cli/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/configuration.ts b/cli/src/api/open-api/configuration.ts index d49752982..c64b2eb3f 100644 --- a/cli/src/api/open-api/configuration.ts +++ b/cli/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/cli/src/api/open-api/index.ts b/cli/src/api/open-api/index.ts index 8dab99636..2afccb668 100644 --- a/cli/src/api/open-api/index.ts +++ b/cli/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 8ada03b28..0381540ea 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.82.0" +version = "1.82.1" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 245c6495b..bbc9a9fe4 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -36,7 +36,7 @@ platform :android do build_type: 'Release', properties: { "android.injected.version.code" => 106, - "android.injected.version.name" => "1.82.0", + "android.injected.version.name" => "1.82.1", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 661af9732..bccdd26cd 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.82.0" + version_number: "1.82.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 8ccdc36a4..c8c913977 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.82.0 +- API version: 1.82.1 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index f902fe7cb..9bae30657 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.82.0+106 +version: 1.82.1+106 isar_version: &isar_version 3.1.0+1 environment: diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 25631ffda..50224f8a0 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -5379,7 +5379,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.82.0", + "version": "1.82.1", "contact": {} }, "tags": [], diff --git a/server/package-lock.json b/server/package-lock.json index a9480d52f..0b54b2f7f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.82.0", + "version": "1.82.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.82.0", + "version": "1.82.1", "license": "UNLICENSED", "dependencies": { "@babel/runtime": "^7.22.11", diff --git a/server/package.json b/server/package.json index e4a51cbbe..acbd2f334 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.82.0", + "version": "1.82.1", "description": "", "author": "", "private": true, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 03fb10f50..d9c34475a 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index 2abcb2b1d..84c4cbb35 100644 --- a/web/src/api/open-api/base.ts +++ b/web/src/api/open-api/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index 41329bb53..39307ae08 100644 --- a/web/src/api/open-api/common.ts +++ b/web/src/api/open-api/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index d49752982..c64b2eb3f 100644 --- a/web/src/api/open-api/configuration.ts +++ b/web/src/api/open-api/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index 8dab99636..2afccb668 100644 --- a/web/src/api/open-api/index.ts +++ b/web/src/api/open-api/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.82.0 + * The version of the OpenAPI document: 1.82.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). From 4b59f832888fa4a7497552f9c730e37df3908436 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 18 Oct 2023 18:02:42 -0400 Subject: [PATCH 07/24] refactor: e2e tests (#4536) --- .github/workflows/test.yml | 2 +- Makefile | 4 +- docker/docker-compose.test.yml | 21 ++--- server/package.json | 2 +- .../src/domain/metadata/metadata.service.ts | 4 + .../repositories/metadata.repository.ts | 1 + .../infra/repositories/metadata.repository.ts | 4 + server/src/microservices/app.service.ts | 4 + server/test/e2e/album.e2e-spec.ts | 79 ++++++++++--------- server/test/e2e/asset.e2e-spec.ts | 23 +++--- server/test/e2e/auth.e2e-spec.ts | 17 ++-- server/test/e2e/formats.e2e-spec.ts | 16 ++-- server/test/e2e/library.e2e-spec.ts | 24 ++---- server/test/e2e/oauth.e2e-spec.ts | 16 ++-- server/test/e2e/partner.e2e-spec.ts | 26 +++--- server/test/e2e/person.e2e-spec.ts | 14 ++-- server/test/e2e/server-info.e2e-spec.ts | 16 ++-- server/test/e2e/setup.ts | 5 +- server/test/e2e/shared-link.e2e-spec.ts | 23 ++---- server/test/e2e/user.e2e-spec.ts | 11 ++- .../repositories/metadata.repository.mock.ts | 1 + server/test/test-utils.ts | 77 ++++++++++-------- 22 files changed, 189 insertions(+), 201 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33b6cf5cc..dac6b1f8a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: submodules: "recursive" - name: Run e2e tests - run: docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build + run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build doc-tests: name: Run documentation checks diff --git a/Makefile b/Makefile index a8b86d75c..504c02c10 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ pull-stage: docker-compose -f ./docker/docker-compose.staging.yml pull test-e2e: - docker-compose -f ./docker/docker-compose.test.yml -p immich-test-e2e up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build + docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build prod: docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans @@ -32,4 +32,4 @@ api: cd ./server && npm run api:generate attach-server: - docker exec -it docker_immich-server_1 sh \ No newline at end of file + docker exec -it docker_immich-server_1 sh diff --git a/docker/docker-compose.test.yml b/docker/docker-compose.test.yml index 57b012334..df965aa1f 100644 --- a/docker/docker-compose.test.yml +++ b/docker/docker-compose.test.yml @@ -1,10 +1,10 @@ version: "3.8" -# Compose file for dockerized end-to-end testing of the backend +name: "immich-test-e2e" services: - immich-server-test: - image: immich-server-test + immich-server: + image: immich-server-dev:latest build: context: ../server dockerfile: Dockerfile @@ -14,27 +14,20 @@ services: - ../server:/usr/src/app - /usr/src/app/node_modules environment: - - DB_HOSTNAME=immich-database-test + - DB_HOSTNAME=database - DB_USERNAME=postgres - DB_PASSWORD=postgres - DB_DATABASE_NAME=e2e_test - IMMICH_RUN_ALL_TESTS=true depends_on: - - immich-database-test - networks: - - immich-test-network + - database - immich-database-test: - container_name: immich-database-test + database: image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441 + command: -c fsync=off environment: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres POSTGRES_DB: e2e_test - networks: - - immich-test-network logging: driver: none - -networks: - immich-test-network: diff --git a/server/package.json b/server/package.json index acbd2f334..def7a64ac 100644 --- a/server/package.json +++ b/server/package.json @@ -26,7 +26,7 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=4096' jest --config test/e2e/jest-e2e.json --runInBand --forceExit", + "test:e2e": "NODE_OPTIONS='--experimental-vm-modules --max_old_space_size=4096' jest --config test/e2e/jest-e2e.json --runInBand", "typeorm": "typeorm", "typeorm:migrations:create": "typeorm migration:create", "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js", diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 2779df54c..9a7b4f3e5 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -109,6 +109,10 @@ export class MetadataService { } } + async teardown() { + await this.repository.teardown(); + } + async handleLivePhotoLinking(job: IEntityJob) { const { id } = job; const [asset] = await this.assetRepository.getByIds([id]); diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts index a037964f4..084a655c7 100644 --- a/server/src/domain/repositories/metadata.repository.ts +++ b/server/src/domain/repositories/metadata.repository.ts @@ -26,6 +26,7 @@ export interface ImmichTags extends Omit { export interface IMetadataRepository { init(options: Partial): Promise; + teardown(): Promise; reverseGeocode(point: GeoPoint): Promise; deleteCache(): Promise; getExifTags(path: string): Promise; diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 3cb53e823..63bc29dcb 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -45,6 +45,10 @@ export class MetadataRepository implements IMetadataRepository { }); } + async teardown() { + await exiftool.end(); + } + async deleteCache() { const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY; if (dumpDirectory) { diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index 1513c6297..365b07329 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -103,4 +103,8 @@ export class AppService { await this.metadataService.init(); await this.searchService.init(); } + + async teardown() { + await this.metadataService.teardown(); + } } diff --git a/server/test/e2e/album.e2e-spec.ts b/server/test/e2e/album.e2e-spec.ts index 633a825a7..e10f5414f 100644 --- a/server/test/e2e/album.e2e-spec.ts +++ b/server/test/e2e/album.e2e-spec.ts @@ -2,11 +2,10 @@ import { AlbumResponseDto, LoginResponseDto } from '@app/domain'; import { AlbumController } from '@app/immich'; import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; import { SharedLinkType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub, uuidStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; const user1SharedUser = 'user1SharedUser'; @@ -17,7 +16,6 @@ const user2SharedLink = 'user2SharedLink'; const user2NotShared = 'user2NotShared'; describe(`${AlbumController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let admin: LoginResponseDto; let user1: LoginResponseDto; @@ -27,9 +25,11 @@ describe(`${AlbumController.name} (e2e)`, () => { let user2Albums: AlbumResponseDto[]; beforeAll(async () => { - app = await createTestApp(); + [server] = await testApp.create(); + }); - server = app.getHttpServer(); + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -37,24 +37,30 @@ describe(`${AlbumController.name} (e2e)`, () => { await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); - await api.userApi.create(server, admin.accessToken, { - email: 'user1@immich.app', - password: 'Password123', - firstName: 'User 1', - lastName: 'Test', - }); - user1 = await api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }); + await Promise.all([ + api.userApi.create(server, admin.accessToken, { + email: 'user1@immich.app', + password: 'Password123', + firstName: 'User 1', + lastName: 'Test', + }), + api.userApi.create(server, admin.accessToken, { + email: 'user2@immich.app', + password: 'Password123', + firstName: 'User 2', + lastName: 'Test', + }), + ]); - await api.userApi.create(server, admin.accessToken, { - email: 'user2@immich.app', - password: 'Password123', - firstName: 'User 2', - lastName: 'Test', - }); - user2 = await api.authApi.login(server, { email: 'user2@immich.app', password: 'Password123' }); + [user1, user2] = await Promise.all([ + api.authApi.login(server, { email: 'user1@immich.app', password: 'Password123' }), + api.authApi.login(server, { email: 'user2@immich.app', password: 'Password123' }), + ]); user1Asset = await api.assetApi.upload(server, user1.accessToken, 'example'); - user1Albums = await Promise.all([ + + const albums = await Promise.all([ + // user 1 api.albumApi.create(server, user1.accessToken, { albumName: user1SharedUser, sharedWithUserIds: [user2.userId], @@ -62,15 +68,8 @@ describe(`${AlbumController.name} (e2e)`, () => { }), api.albumApi.create(server, user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset.id] }), api.albumApi.create(server, user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset.id] }), - ]); - // add shared link to user1SharedLink album - await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: user1Albums[1].id, - }); - - user2Albums = await Promise.all([ + // user 2 api.albumApi.create(server, user2.accessToken, { albumName: user2SharedUser, sharedWithUserIds: [user1.userId], @@ -80,16 +79,22 @@ describe(`${AlbumController.name} (e2e)`, () => { api.albumApi.create(server, user2.accessToken, { albumName: user2NotShared }), ]); - // add shared link to user2SharedLink album - await api.sharedLinkApi.create(server, user2.accessToken, { - type: SharedLinkType.ALBUM, - albumId: user2Albums[1].id, - }); - }); + user1Albums = albums.slice(0, 3); + user2Albums = albums.slice(3); - afterAll(async () => { - await db.disconnect(); - await app.close(); + await Promise.all([ + // add shared link to user1SharedLink album + api.sharedLinkApi.create(server, user1.accessToken, { + type: SharedLinkType.ALBUM, + albumId: user1Albums[1].id, + }), + + // add shared link to user2SharedLink album + api.sharedLinkApi.create(server, user2.accessToken, { + type: SharedLinkType.ALBUM, + albumId: user2Albums[1].id, + }), + ]); }); describe('GET /album', () => { diff --git a/server/test/e2e/asset.e2e-spec.ts b/server/test/e2e/asset.e2e-spec.ts index 4f4021d59..c18268502 100644 --- a/server/test/e2e/asset.e2e-spec.ts +++ b/server/test/e2e/asset.e2e-spec.ts @@ -12,7 +12,7 @@ import { AssetEntity, AssetType, SharedLinkType } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { errorStub, uuidStub } from '@test/fixtures'; -import { createTestApp, db } from '@test/test-utils'; +import { db, testApp } from '@test/test-utils'; import { randomBytes } from 'crypto'; import request from 'supertest'; @@ -86,12 +86,14 @@ describe(`${AssetController.name} (e2e)`, () => { let asset4: AssetEntity; beforeAll(async () => { - app = await createTestApp(); - - server = app.getHttpServer(); + [server, app] = await testApp.create(); assetRepository = app.get(IAssetRepository); }); + afterAll(async () => { + await testApp.teardown(); + }); + beforeEach(async () => { await db.reset(); await api.authApi.adminSignUp(server); @@ -123,11 +125,6 @@ describe(`${AssetController.name} (e2e)`, () => { }); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('POST /asset/upload', () => { it('should require authentication', async () => { const { status, body } = await request(server) @@ -589,9 +586,11 @@ describe(`${AssetController.name} (e2e)`, () => { describe('GET /asset/map-marker', () => { beforeEach(async () => { - await assetRepository.save({ id: asset1.id, isArchived: true }); - await assetRepository.upsertExif({ assetId: asset1.id, latitude: 0, longitude: 0 }); - await assetRepository.upsertExif({ assetId: asset2.id, latitude: 0, longitude: 0 }); + await Promise.all([ + assetRepository.save({ id: asset1.id, isArchived: true }), + assetRepository.upsertExif({ assetId: asset1.id, latitude: 0, longitude: 0 }), + assetRepository.upsertExif({ assetId: asset2.id, latitude: 0, longitude: 0 }), + ]); }); it('should require authentication', async () => { diff --git a/server/test/e2e/auth.e2e-spec.ts b/server/test/e2e/auth.e2e-spec.ts index 4068634e7..a42e1e161 100644 --- a/server/test/e2e/auth.e2e-spec.ts +++ b/server/test/e2e/auth.e2e-spec.ts @@ -1,5 +1,4 @@ import { AuthController } from '@app/immich'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { @@ -12,7 +11,7 @@ import { signupResponseStub, uuidStub, } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; const firstName = 'Immich'; @@ -21,13 +20,16 @@ const password = 'Password123'; const email = 'admin@immich.app'; describe(`${AuthController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let accessToken: string; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + await testApp.reset(); + [server] = await testApp.create(); + }); + + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -37,11 +39,6 @@ describe(`${AuthController.name} (e2e)`, () => { accessToken = response.accessToken; }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('POST /auth/admin-sign-up', () => { beforeEach(async () => { await db.reset(); diff --git a/server/test/e2e/formats.e2e-spec.ts b/server/test/e2e/formats.e2e-spec.ts index 98e24ec9a..f2fce83ac 100644 --- a/server/test/e2e/formats.e2e-spec.ts +++ b/server/test/e2e/formats.e2e-spec.ts @@ -1,11 +1,9 @@ import { LoginResponseDto } from '@app/domain'; import { AssetType, LibraryType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { IMMICH_TEST_ASSET_PATH, createTestApp, db, runAllTests } from '@test/test-utils'; +import { IMMICH_TEST_ASSET_PATH, db, runAllTests, testApp } from '@test/test-utils'; describe(`Supported file formats (e2e)`, () => { - let app: INestApplication; let server: any; let admin: LoginResponseDto; @@ -170,8 +168,11 @@ describe(`Supported file formats (e2e)`, () => { const testsToRun = formatTests.filter((formatTest) => formatTest.runTest); beforeAll(async () => { - app = await createTestApp(true); - server = app.getHttpServer(); + [server] = await testApp.create({ jobs: true }); + }); + + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -181,11 +182,6 @@ describe(`Supported file formats (e2e)`, () => { await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - it.each(testsToRun)('should import file of format $format', async (testedFormat) => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, diff --git a/server/test/e2e/library.e2e-spec.ts b/server/test/e2e/library.e2e-spec.ts index 742e6b7fe..9cfbe8961 100644 --- a/server/test/e2e/library.e2e-spec.ts +++ b/server/test/e2e/library.e2e-spec.ts @@ -1,22 +1,14 @@ import { LibraryResponseDto, LoginResponseDto } from '@app/domain'; import { LibraryController } from '@app/immich'; import { AssetType, LibraryType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; -import { - IMMICH_TEST_ASSET_PATH, - IMMICH_TEST_ASSET_TEMP_PATH, - createTestApp, - db, - restoreTempFolder, -} from '@test/test-utils'; +import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, db, restoreTempFolder, testApp } from '@test/test-utils'; import * as fs from 'fs'; import request from 'supertest'; import { utimes } from 'utimes'; import { errorStub, uuidStub } from '../fixtures'; describe(`${LibraryController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let admin: LoginResponseDto; @@ -35,8 +27,12 @@ describe(`${LibraryController.name} (e2e)`, () => { }; beforeAll(async () => { - app = await createTestApp(true); - server = app.getHttpServer(); + [server] = await testApp.create({ jobs: true }); + }); + + afterAll(async () => { + await testApp.teardown(); + await restoreTempFolder(); }); beforeEach(async () => { @@ -46,12 +42,6 @@ describe(`${LibraryController.name} (e2e)`, () => { admin = await api.authApi.adminLogin(server); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - await restoreTempFolder(); - }); - describe('GET /library', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/library'); diff --git a/server/test/e2e/oauth.e2e-spec.ts b/server/test/e2e/oauth.e2e-spec.ts index d0d2137c6..879d53815 100644 --- a/server/test/e2e/oauth.e2e-spec.ts +++ b/server/test/e2e/oauth.e2e-spec.ts @@ -1,18 +1,19 @@ import { OAuthController } from '@app/immich'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; describe(`${OAuthController.name} (e2e)`, () => { - let app: INestApplication; let server: any; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + [server] = await testApp.create(); + }); + + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -20,11 +21,6 @@ describe(`${OAuthController.name} (e2e)`, () => { await api.authApi.adminSignUp(server); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('POST /oauth/authorize', () => { beforeEach(async () => { await db.reset(); diff --git a/server/test/e2e/partner.e2e-spec.ts b/server/test/e2e/partner.e2e-spec.ts index b0eb1d4ce..82a09dcc8 100644 --- a/server/test/e2e/partner.e2e-spec.ts +++ b/server/test/e2e/partner.e2e-spec.ts @@ -4,7 +4,7 @@ import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; const user1Dto = { @@ -31,27 +31,29 @@ describe(`${PartnerController.name} (e2e)`, () => { let user2: LoginResponseDto; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + [server, app] = await testApp.create(); repository = app.get(IPartnerRepository); }); + afterAll(async () => { + await testApp.teardown(); + }); + beforeEach(async () => { await db.reset(); await api.authApi.adminSignUp(server); loginResponse = await api.authApi.adminLogin(server); accessToken = loginResponse.accessToken; - await api.userApi.create(server, accessToken, user1Dto); - user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); + await Promise.all([ + api.userApi.create(server, accessToken, user1Dto), + api.userApi.create(server, accessToken, user2Dto), + ]); - await api.userApi.create(server, accessToken, user2Dto); - user2 = await api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }); - }); - - afterAll(async () => { - await db.disconnect(); - await app.close(); + [user1, user2] = await Promise.all([ + api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }), + api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }), + ]); }); describe('GET /partner', () => { diff --git a/server/test/e2e/person.e2e-spec.ts b/server/test/e2e/person.e2e-spec.ts index f9da56fa8..bb0af4c96 100644 --- a/server/test/e2e/person.e2e-spec.ts +++ b/server/test/e2e/person.e2e-spec.ts @@ -5,7 +5,7 @@ import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub, uuidStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; describe(`${PersonController.name}`, () => { @@ -18,11 +18,14 @@ describe(`${PersonController.name}`, () => { let hiddenPerson: PersonEntity; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + [server, app] = await testApp.create(); personRepository = app.get(IPersonRepository); }); + afterAll(async () => { + await testApp.teardown(); + }); + beforeEach(async () => { await db.reset(); await api.authApi.adminSignUp(server); @@ -46,11 +49,6 @@ describe(`${PersonController.name}`, () => { await personRepository.createFace({ assetId: faceAsset.id, personId: hiddenPerson.id }); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('GET /person', () => { beforeEach(async () => {}); diff --git a/server/test/e2e/server-info.e2e-spec.ts b/server/test/e2e/server-info.e2e-spec.ts index 43cf471f4..cd6afbc07 100644 --- a/server/test/e2e/server-info.e2e-spec.ts +++ b/server/test/e2e/server-info.e2e-spec.ts @@ -1,21 +1,22 @@ import { LoginResponseDto } from '@app/domain'; import { ServerInfoController } from '@app/immich'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; describe(`${ServerInfoController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let accessToken: string; let loginResponse: LoginResponseDto; beforeAll(async () => { - app = await createTestApp(); - server = app.getHttpServer(); + [server] = await testApp.create(); + }); + + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { @@ -25,11 +26,6 @@ describe(`${ServerInfoController.name} (e2e)`, () => { accessToken = loginResponse.accessToken; }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - }); - describe('GET /server-info', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/server-info'); diff --git a/server/test/e2e/setup.ts b/server/test/e2e/setup.ts index 26849f468..234deb754 100644 --- a/server/test/e2e/setup.ts +++ b/server/test/e2e/setup.ts @@ -1,5 +1,5 @@ import { PostgreSqlContainer } from '@testcontainers/postgresql'; -import * as fs from 'fs'; +import { access } from 'fs/promises'; import path from 'path'; export default async () => { @@ -23,8 +23,7 @@ export default async () => { } const directoryExists = async (dirPath: string) => - await fs.promises - .access(dirPath) + await access(dirPath) .then(() => true) .catch(() => false); diff --git a/server/test/e2e/shared-link.e2e-spec.ts b/server/test/e2e/shared-link.e2e-spec.ts index 3a52c15a0..80d43c7c7 100644 --- a/server/test/e2e/shared-link.e2e-spec.ts +++ b/server/test/e2e/shared-link.e2e-spec.ts @@ -1,16 +1,10 @@ import { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto } from '@app/domain'; import { PartnerController } from '@app/immich'; import { LibraryType, SharedLinkType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub, uuidStub } from '@test/fixtures'; -import { - IMMICH_TEST_ASSET_PATH, - IMMICH_TEST_ASSET_TEMP_PATH, - createTestApp, - restoreTempFolder, -} from '@test/test-utils'; +import { IMMICH_TEST_ASSET_PATH, IMMICH_TEST_ASSET_TEMP_PATH, restoreTempFolder, testApp } from '@test/test-utils'; import { cp } from 'fs/promises'; import request from 'supertest'; @@ -22,7 +16,6 @@ const user1Dto = { }; describe(`${PartnerController.name} (e2e)`, () => { - let app: INestApplication; let server: any; let admin: LoginResponseDto; let user1: LoginResponseDto; @@ -30,8 +23,12 @@ describe(`${PartnerController.name} (e2e)`, () => { let sharedLink: SharedLinkResponseDto; beforeAll(async () => { - app = await createTestApp(true); - server = app.getHttpServer(); + [server] = await testApp.create({ jobs: true }); + }); + + afterAll(async () => { + await testApp.teardown(); + await restoreTempFolder(); }); beforeEach(async () => { @@ -49,12 +46,6 @@ describe(`${PartnerController.name} (e2e)`, () => { }); }); - afterAll(async () => { - await db.disconnect(); - await app.close(); - await restoreTempFolder(); - }); - describe('GET /shared-link', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/shared-link'); diff --git a/server/test/e2e/user.e2e-spec.ts b/server/test/e2e/user.e2e-spec.ts index af0cbde74..d20ac729f 100644 --- a/server/test/e2e/user.e2e-spec.ts +++ b/server/test/e2e/user.e2e-spec.ts @@ -2,10 +2,11 @@ import { LoginResponseDto, UserResponseDto, UserService } from '@app/domain'; import { AppModule, UserController } from '@app/immich'; import { UserEntity } from '@app/infra/entities'; import { INestApplication } from '@nestjs/common'; +import { getRepositoryToken } from '@nestjs/typeorm'; import { api } from '@test/api'; import { db } from '@test/db'; import { errorStub, userSignupStub, userStub } from '@test/fixtures'; -import { createTestApp } from '@test/test-utils'; +import { testApp } from '@test/test-utils'; import request from 'supertest'; import { Repository } from 'typeorm'; @@ -18,10 +19,12 @@ describe(`${UserController.name}`, () => { let userRepository: Repository; beforeAll(async () => { - app = await createTestApp(); - userRepository = app.select(AppModule).get('UserEntityRepository'); + [server, app] = await testApp.create(); + userRepository = app.select(AppModule).get(getRepositoryToken(UserEntity)); + }); - server = app.getHttpServer(); + afterAll(async () => { + await testApp.teardown(); }); beforeEach(async () => { diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 13589f15b..76c6f777a 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -5,6 +5,7 @@ export const newMetadataRepositoryMock = (): jest.Mocked => deleteCache: jest.fn(), getExifTags: jest.fn(), init: jest.fn(), + teardown: jest.fn(), reverseGeocode: jest.fn(), }; }; diff --git a/server/test/test-utils.ts b/server/test/test-utils.ts index 075e0b69f..6b45c6ee6 100644 --- a/server/test/test-utils.ts +++ b/server/test/test-utils.ts @@ -1,9 +1,8 @@ -import { dataSource } from '@app/infra'; - import { IJobRepository, JobItem, JobItemHandler, QueueName } from '@app/domain'; import { AppModule } from '@app/immich'; -import { INestApplication, Logger } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; +import { dataSource } from '@app/infra'; +import { INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; import * as fs from 'fs'; import path from 'path'; import { AppService } from '../src/microservices/app.service'; @@ -36,38 +35,48 @@ export const db = { let _handler: JobItemHandler = () => Promise.resolve(); -export async function createTestApp(runJobs = false, log = false): Promise { - const moduleBuilder = Test.createTestingModule({ - imports: [AppModule], - providers: [AppService], - }) - .overrideProvider(IJobRepository) - .useValue({ - addHandler: (_queueName: QueueName, _concurrency: number, handler: JobItemHandler) => (_handler = handler), - queue: (item: JobItem) => runJobs && _handler(item), - resume: jest.fn(), - empty: jest.fn(), - setConcurrency: jest.fn(), - getQueueStatus: jest.fn(), - getJobCounts: jest.fn(), - pause: jest.fn(), - } as IJobRepository); - - const moduleFixture: TestingModule = await moduleBuilder.compile(); - - const app = moduleFixture.createNestApplication(); - if (log) { - app.useLogger(new Logger()); - } else { - app.useLogger(false); - } - await app.init(); - const appService = app.get(AppService); - await appService.init(); - - return app; +interface TestAppOptions { + jobs: boolean; } +let app: INestApplication; + +export const testApp = { + create: async (options?: TestAppOptions): Promise<[any, INestApplication]> => { + const { jobs } = options || { jobs: false }; + + const moduleFixture = await Test.createTestingModule({ imports: [AppModule], providers: [AppService] }) + .overrideProvider(IJobRepository) + .useValue({ + addHandler: (_queueName: QueueName, _concurrency: number, handler: JobItemHandler) => (_handler = handler), + queue: (item: JobItem) => jobs && _handler(item), + resume: jest.fn(), + empty: jest.fn(), + setConcurrency: jest.fn(), + getQueueStatus: jest.fn(), + getJobCounts: jest.fn(), + pause: jest.fn(), + } as IJobRepository) + .compile(); + + app = await moduleFixture.createNestApplication().init(); + + if (jobs) { + await app.get(AppService).init(); + } + + return [app.getHttpServer(), app]; + }, + reset: async () => { + await db.reset(); + }, + teardown: async () => { + await app.get(AppService).teardown(); + await db.disconnect(); + await app.close(); + }, +}; + export const runAllTests: boolean = process.env.IMMICH_RUN_ALL_TESTS === 'true'; const directoryExists = async (dirPath: string) => From 5a7ef02387aea9db3774c8a60dc02663e57e973c Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Thu, 19 Oct 2023 04:46:06 +0200 Subject: [PATCH 08/24] refactor(web): Allow dropdown for more general use (#4515) --- .../lib/components/elements/dropdown.svelte | 72 ++++++++++++------- web/src/routes/(user)/albums/+page.svelte | 13 ++-- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/web/src/lib/components/elements/dropdown.svelte b/web/src/lib/components/elements/dropdown.svelte index 570b5f84d..81d3ecdf6 100644 --- a/web/src/lib/components/elements/dropdown.svelte +++ b/web/src/lib/components/elements/dropdown.svelte @@ -1,17 +1,31 @@ - + +