import { LibraryResponseDto, LoginResponseDto } from '@app/domain'; import { LibraryController } from '@app/immich'; import { AssetType, LibraryType } from '@app/infra/entities'; import { api } from '@test/api'; 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 server: any; let admin: LoginResponseDto; const user1Dto = { email: 'user1@immich.app', password: 'Password123', firstName: 'User 1', lastName: 'Test', }; const user2Dto = { email: 'user2@immich.app', password: 'Password123', firstName: 'User 2', lastName: 'Test', }; beforeAll(async () => { [server] = await testApp.create({ jobs: true }); }); afterAll(async () => { await testApp.teardown(); await restoreTempFolder(); }); beforeEach(async () => { await db.reset(); await restoreTempFolder(); await api.authApi.adminSignUp(server); admin = await api.authApi.adminLogin(server); }); describe('GET /library', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/library'); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); it('should start with a default upload library', async () => { const { status, body } = await request(server) .get('/library') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); expect(body).toHaveLength(1); expect(body).toEqual([ expect.objectContaining({ ownerId: admin.userId, type: LibraryType.UPLOAD, name: 'Default Library', refreshedAt: null, assetCount: 0, importPaths: [], exclusionPatterns: [], }), ]); }); }); describe('POST /library', () => { it('should require authentication', async () => { const { status, body } = await request(server).post('/library').send({}); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); describe('external library', () => { it('with default settings', async () => { const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ type: LibraryType.EXTERNAL }); expect(status).toBe(201); expect(body).toEqual( expect.objectContaining({ ownerId: admin.userId, type: LibraryType.EXTERNAL, name: 'New External Library', refreshedAt: null, assetCount: 0, importPaths: [], exclusionPatterns: [], }), ); }); it('with name', async () => { const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ type: LibraryType.EXTERNAL, name: 'My Awesome Library' }); expect(status).toBe(201); expect(body).toEqual( expect.objectContaining({ name: 'My Awesome Library', }), ); }); it('with import paths', async () => { const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ type: LibraryType.EXTERNAL, importPaths: ['/path/to/import'] }); expect(status).toBe(201); expect(body).toEqual( expect.objectContaining({ importPaths: ['/path/to/import'], }), ); }); it('with exclusion patterns', async () => { const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ type: LibraryType.EXTERNAL, exclusionPatterns: ['**/Raw/**'] }); expect(status).toBe(201); expect(body).toEqual( expect.objectContaining({ exclusionPatterns: ['**/Raw/**'], }), ); }); }); describe('upload library', () => { it('with default settings', async () => { const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ type: LibraryType.UPLOAD }); expect(status).toBe(201); expect(body).toEqual( expect.objectContaining({ ownerId: admin.userId, type: LibraryType.UPLOAD, name: 'New Upload Library', refreshedAt: null, assetCount: 0, importPaths: [], exclusionPatterns: [], }), ); }); it('with name', async () => { const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ type: LibraryType.UPLOAD, name: 'My Awesome Library' }); expect(status).toBe(201); expect(body).toEqual( expect.objectContaining({ name: 'My Awesome Library', }), ); }); it('with import paths should fail', async () => { const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ type: LibraryType.UPLOAD, importPaths: ['/path/to/import'] }); expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest('Upload libraries cannot have import paths')); }); it('with exclusion patterns should fail', async () => { const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ type: LibraryType.UPLOAD, exclusionPatterns: ['**/Raw/**'] }); expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest('Upload libraries cannot have exclusion patterns')); }); }); it('should allow a user to create a library', async () => { await api.userApi.create(server, admin.accessToken, user1Dto); const user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); const { status, body } = await request(server) .post('/library') .set('Authorization', `Bearer ${user1.accessToken}`) .send({ type: LibraryType.EXTERNAL }); expect(status).toBe(201); expect(body).toEqual( expect.objectContaining({ ownerId: user1.userId, type: LibraryType.EXTERNAL, name: 'New External Library', refreshedAt: null, assetCount: 0, importPaths: [], exclusionPatterns: [], }), ); }); }); describe('PUT /library/:id', () => { it('should require authentication', async () => { const { status, body } = await request(server).put(`/library/${uuidStub.notFound}`).send({}); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); describe('external library', () => { let library: LibraryResponseDto; beforeEach(async () => { // Create an external library with default settings library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL }); }); it('should change the library name', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) .send({ name: 'New Library Name' }); expect(status).toBe(200); expect(body).toEqual( expect.objectContaining({ name: 'New Library Name', }), ); }); it('should not set an empty name', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) .send({ name: '' }); expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest(['name should not be empty'])); }); it('should change the import paths', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) .send({ importPaths: ['/path/to/import'] }); expect(status).toBe(200); expect(body).toEqual( expect.objectContaining({ importPaths: ['/path/to/import'], }), ); }); it('should not allow an empty import path', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) .send({ importPaths: [''] }); expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest(['each value in importPaths should not be empty'])); }); it('should change the exclusion pattern', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) .send({ exclusionPatterns: ['**/Raw/**'] }); expect(status).toBe(200); expect(body).toEqual( expect.objectContaining({ exclusionPatterns: ['**/Raw/**'], }), ); }); it('should not allow an empty exclusion pattern', async () => { const { status, body } = await request(server) .put(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) .send({ exclusionPatterns: [''] }); expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest(['each value in exclusionPatterns should not be empty'])); }); }); }); describe('GET /library/:id', () => { it('should require authentication', async () => { const { status, body } = await request(server).get(`/library/${uuidStub.notFound}`); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); it('should get library by id', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL }); const { status, body } = await request(server) .get(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); expect(body).toEqual( expect.objectContaining({ ownerId: admin.userId, type: LibraryType.EXTERNAL, name: 'New External Library', refreshedAt: null, assetCount: 0, importPaths: [], exclusionPatterns: [], }), ); }); it("should not allow getting another user's library", async () => { await api.userApi.create(server, admin.accessToken, user1Dto); const user1 = await api.authApi.login(server, { email: user1Dto.email, password: user1Dto.password }); await api.userApi.create(server, admin.accessToken, user2Dto); const user2 = await api.authApi.login(server, { email: user2Dto.email, password: user2Dto.password }); const library = await api.libraryApi.create(server, user1.accessToken, { type: LibraryType.EXTERNAL }); const { status, body } = await request(server) .get(`/library/${library.id}`) .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest('Not found or no library.read access')); }); }); describe('DELETE /library/:id', () => { it('should require authentication', async () => { const { status, body } = await request(server).delete(`/library/${uuidStub.notFound}`); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); it('should not delete the last upload library', async () => { const [defaultLibrary] = await api.libraryApi.getAll(server, admin.accessToken); expect(defaultLibrary).toBeDefined(); const { status, body } = await request(server) .delete(`/library/${defaultLibrary.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); expect(body).toEqual(errorStub.noDeleteUploadLibrary); }); it('should delete an empty library', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL }); const { status, body } = await request(server) .delete(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); expect(body).toEqual({}); const libraries = await api.libraryApi.getAll(server, admin.accessToken); expect(libraries).toHaveLength(1); expect(libraries).not.toEqual( expect.arrayContaining([ expect.objectContaining({ id: library.id, }), ]), ); }); it('should delete an external library with assets', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets.length).toBeGreaterThan(2); const { status, body } = await request(server) .delete(`/library/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); expect(body).toEqual({}); const libraries = await api.libraryApi.getAll(server, admin.accessToken); expect(libraries).toHaveLength(1); expect(libraries).not.toEqual( expect.arrayContaining([ expect.objectContaining({ id: library.id, }), ]), ); }); }); describe('GET /library/:id/statistics', () => { it('should require authentication', async () => { const { status, body } = await request(server).get(`/library/${uuidStub.notFound}/statistics`); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); }); describe('POST /library/:id/scan', () => { it('should require authentication', async () => { const { status, body } = await request(server).post(`/library/${uuidStub.notFound}/scan`).send({}); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); it('should scan external library with import paths', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual( expect.arrayContaining([ expect.objectContaining({ type: AssetType.IMAGE, originalFileName: 'el_torcal_rocks', libraryId: library.id, resized: true, thumbhash: expect.any(String), exifInfo: expect.objectContaining({ exifImageWidth: 512, exifImageHeight: 341, latitude: null, longitude: null, }), }), expect.objectContaining({ type: AssetType.IMAGE, originalFileName: 'silver_fir', libraryId: library.id, resized: true, thumbhash: expect.any(String), exifInfo: expect.objectContaining({ exifImageWidth: 511, exifImageHeight: 323, latitude: null, longitude: null, }), }), ]), ); }); it('should scan external library with exclusion pattern', async () => { await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path'); const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], exclusionPatterns: ['**/el_corcal*'], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual( expect.arrayContaining([ expect.not.objectContaining({ // Excluded by exclusion pattern originalFileName: 'el_torcal_rocks', }), expect.objectContaining({ type: AssetType.IMAGE, originalFileName: 'silver_fir', libraryId: library.id, resized: true, exifInfo: expect.objectContaining({ exifImageWidth: 511, exifImageHeight: 323, latitude: null, longitude: null, }), }), ]), ); }); it('should scan external library with import paths', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual( expect.arrayContaining([ expect.objectContaining({ type: AssetType.IMAGE, originalFileName: 'el_torcal_rocks', libraryId: library.id, resized: true, exifInfo: expect.objectContaining({ exifImageWidth: 512, exifImageHeight: 341, latitude: null, longitude: null, }), }), expect.objectContaining({ type: AssetType.IMAGE, originalFileName: 'silver_fir', libraryId: library.id, resized: true, thumbhash: expect.any(String), exifInfo: expect.objectContaining({ exifImageWidth: 511, exifImageHeight: 323, latitude: null, longitude: null, }), }), ]), ); }); it('should offline missing files', async () => { await fs.promises.cp(`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_TEMP_PATH}/albums/nature`, { recursive: true, }); const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const onlineAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(onlineAssets.length).toBeGreaterThan(1); await restoreTempFolder(); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual( expect.arrayContaining([ expect.objectContaining({ isOffline: true, originalFileName: 'el_torcal_rocks', }), expect.objectContaining({ isOffline: true, originalFileName: 'tanners_ridge', }), ]), ); }); it('should offline files outside of changed external path', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/some/other/path'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual( expect.arrayContaining([ expect.objectContaining({ isOffline: true, originalFileName: 'el_torcal_rocks', }), expect.objectContaining({ isOffline: true, originalFileName: 'tanners_ridge', }), ]), ); }); it('should scan new files', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/silver_fir.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/silver_fir.jpg`, ); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual( expect.arrayContaining([ expect.objectContaining({ originalFileName: 'el_torcal_rocks', }), expect.objectContaining({ originalFileName: 'silver_fir', }), ]), ); }); describe('with refreshModifiedFiles=true', () => { it('should reimport modified files', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200001); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshModifiedFiles: true }); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets.length).toBe(1); expect(assets[0]).toEqual( expect.objectContaining({ originalFileName: 'el_torcal_rocks', exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-09-25T08:33:30.880Z', exifImageHeight: 534, exifImageWidth: 800, exposureTime: '1/15', fNumber: 22, fileSizeInByte: 114225, focalLength: 35, iso: 1000, make: 'NIKON CORPORATION', model: 'NIKON D750', }), }), ); }); it('should not reimport unmodified files', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshModifiedFiles: true }); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets.length).toBe(1); expect(assets[0]).toEqual( expect.objectContaining({ originalFileName: 'el_torcal_rocks', exifInfo: expect.objectContaining({ dateTimeOriginal: '2012-08-05T11:39:59.000Z', }), }), ); }); }); describe('with refreshAllFiles=true', () => { it('should reimport all files', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); await fs.promises.cp( `${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`, `${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, ); await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447775200000); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshAllFiles: true }); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets.length).toBe(1); expect(assets[0]).toEqual( expect.objectContaining({ originalFileName: 'el_torcal_rocks', exifInfo: expect.objectContaining({ exifImageHeight: 534, exifImageWidth: 800, exposureTime: '1/15', fNumber: 22, fileSizeInByte: 114225, focalLength: 35, iso: 1000, make: 'NIKON CORPORATION', model: 'NIKON D750', }), }), ); }); }); describe('External path', () => { let library: LibraryResponseDto; beforeEach(async () => { library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); }); it('should not scan assets for user without external path', async () => { await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual([]); }); it("should not import assets outside of user's external path", async () => { await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/not/a/real/path'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual([]); }); it.each([`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_PATH}/albums/nature/`])( 'should scan external library with external path %s', async (externalPath: string) => { await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, externalPath); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual( expect.arrayContaining([ expect.objectContaining({ type: AssetType.IMAGE, originalFileName: 'el_torcal_rocks', libraryId: library.id, resized: true, exifInfo: expect.objectContaining({ exifImageWidth: 512, exifImageHeight: 341, latitude: null, longitude: null, }), }), expect.objectContaining({ type: AssetType.IMAGE, originalFileName: 'silver_fir', libraryId: library.id, resized: true, exifInfo: expect.objectContaining({ exifImageWidth: 511, exifImageHeight: 323, latitude: null, longitude: null, }), }), ]), ); }, ); }); it('should not scan an upload library', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.UPLOAD, }); const { status, body } = await request(server) .post(`/library/${library.id}/scan`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); expect(body).toEqual(errorStub.badRequest('Can only refresh external libraries')); }); }); describe('POST /library/:id/removeOffline', () => { it('should require authentication', async () => { const { status, body } = await request(server).post(`/library/${uuidStub.notFound}/removeOffline`).send({}); expect(status).toBe(401); expect(body).toEqual(errorStub.unauthorized); }); it('should remvove offline files', async () => { await fs.promises.cp(`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_TEMP_PATH}/albums/nature`, { recursive: true, }); const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const onlineAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(onlineAssets.length).toBeGreaterThan(1); await restoreTempFolder(); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const { status } = await request(server) .post(`/library/${library.id}/removeOffline`) .set('Authorization', `Bearer ${admin.accessToken}`) .send(); expect(status).toBe(201); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets).toEqual([]); }); it('should not remvove online files', async () => { const library = await api.libraryApi.create(server, admin.accessToken, { type: LibraryType.EXTERNAL, importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`], }); await api.userApi.setExternalPath(server, admin.accessToken, admin.userId, '/'); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const assetsBefore = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assetsBefore.length).toBeGreaterThan(1); await api.libraryApi.scanLibrary(server, admin.accessToken, library.id); const { status } = await request(server) .post(`/library/${library.id}/removeOffline`) .set('Authorization', `Bearer ${admin.accessToken}`) .send(); expect(status).toBe(201); const assetsAfter = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assetsAfter).toEqual(assetsBefore); }); }); });