mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	* feat: libraries Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
		
			
				
	
	
		
			495 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			495 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { LoginResponseDto } from '@app/domain';
 | 
						|
import { AppModule, LibraryController } from '@app/immich';
 | 
						|
import { LibraryType } from '@app/infra/entities';
 | 
						|
import { INestApplication } from '@nestjs/common';
 | 
						|
import { Test, TestingModule } from '@nestjs/testing';
 | 
						|
import request from 'supertest';
 | 
						|
import { errorStub, userStub, uuidStub } from '../fixtures';
 | 
						|
import { api, db } from '../test-utils';
 | 
						|
 | 
						|
describe(`${LibraryController.name} (e2e)`, () => {
 | 
						|
  let app: INestApplication;
 | 
						|
  let server: any;
 | 
						|
  let loginResponse: LoginResponseDto;
 | 
						|
  let accessToken: string;
 | 
						|
 | 
						|
  beforeAll(async () => {
 | 
						|
    const moduleFixture: TestingModule = await Test.createTestingModule({
 | 
						|
      imports: [AppModule],
 | 
						|
    }).compile();
 | 
						|
 | 
						|
    app = await moduleFixture.createNestApplication().init();
 | 
						|
    server = app.getHttpServer();
 | 
						|
  });
 | 
						|
 | 
						|
  beforeEach(async () => {
 | 
						|
    await db.reset();
 | 
						|
    await api.adminSignUp(server);
 | 
						|
    loginResponse = await api.adminLogin(server);
 | 
						|
    accessToken = loginResponse.accessToken;
 | 
						|
  });
 | 
						|
 | 
						|
  afterAll(async () => {
 | 
						|
    await db.disconnect();
 | 
						|
    await app.close();
 | 
						|
  });
 | 
						|
 | 
						|
  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 ${accessToken}`);
 | 
						|
      expect(status).toBe(200);
 | 
						|
      expect(body).toHaveLength(1);
 | 
						|
      expect(body).toEqual([
 | 
						|
        {
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.UPLOAD,
 | 
						|
          name: 'Default Library',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          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 ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.EXTERNAL });
 | 
						|
        expect(status).toBe(201);
 | 
						|
 | 
						|
        expect(body).toEqual({
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.EXTERNAL,
 | 
						|
          name: 'New External Library',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          refreshedAt: null,
 | 
						|
          assetCount: 0,
 | 
						|
          importPaths: [],
 | 
						|
          exclusionPatterns: [],
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      it('with name', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.EXTERNAL, name: 'My Awesome Library' });
 | 
						|
        expect(status).toBe(201);
 | 
						|
 | 
						|
        expect(body).toEqual({
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.EXTERNAL,
 | 
						|
          name: 'My Awesome Library',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          refreshedAt: null,
 | 
						|
          assetCount: 0,
 | 
						|
          importPaths: [],
 | 
						|
          exclusionPatterns: [],
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      it('with import paths', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.EXTERNAL, importPaths: ['/path/to/import'] });
 | 
						|
        expect(status).toBe(201);
 | 
						|
 | 
						|
        expect(body).toEqual({
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.EXTERNAL,
 | 
						|
          name: 'New External Library',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          refreshedAt: null,
 | 
						|
          assetCount: 0,
 | 
						|
          importPaths: ['/path/to/import'],
 | 
						|
          exclusionPatterns: [],
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      it('with exclusion patterns', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.EXTERNAL, exclusionPatterns: ['**/Raw/**'] });
 | 
						|
        expect(status).toBe(201);
 | 
						|
 | 
						|
        expect(body).toEqual({
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.EXTERNAL,
 | 
						|
          name: 'New External Library',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          refreshedAt: null,
 | 
						|
          assetCount: 0,
 | 
						|
          importPaths: [],
 | 
						|
          exclusionPatterns: ['**/Raw/**'],
 | 
						|
        });
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    describe('upload library', () => {
 | 
						|
      it('with default settings', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.UPLOAD });
 | 
						|
        expect(status).toBe(201);
 | 
						|
 | 
						|
        expect(body).toEqual({
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.UPLOAD,
 | 
						|
          name: 'New Upload Library',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          refreshedAt: null,
 | 
						|
          assetCount: 0,
 | 
						|
          importPaths: [],
 | 
						|
          exclusionPatterns: [],
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      it('with name', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.UPLOAD, name: 'My Awesome Library' });
 | 
						|
        expect(status).toBe(201);
 | 
						|
 | 
						|
        expect(body).toEqual({
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.UPLOAD,
 | 
						|
          name: 'My Awesome Library',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          refreshedAt: null,
 | 
						|
          assetCount: 0,
 | 
						|
          importPaths: [],
 | 
						|
          exclusionPatterns: [],
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      it('with import paths should fail', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${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 ${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.userCreate(server, accessToken, userStub.user1);
 | 
						|
 | 
						|
      const loginResponse = await api.login(server, {
 | 
						|
        email: userStub.user1.email,
 | 
						|
        password: userStub.user1.password ?? '',
 | 
						|
      });
 | 
						|
 | 
						|
      const { status, body } = await request(server)
 | 
						|
        .post('/library')
 | 
						|
        .set('Authorization', `Bearer ${loginResponse.accessToken}`)
 | 
						|
        .send({ type: LibraryType.EXTERNAL });
 | 
						|
 | 
						|
      expect(status).toBe(201);
 | 
						|
      expect(body).toEqual({
 | 
						|
        id: expect.any(String),
 | 
						|
        ownerId: loginResponse.userId,
 | 
						|
        type: LibraryType.EXTERNAL,
 | 
						|
        name: 'New External Library',
 | 
						|
        createdAt: expect.any(String),
 | 
						|
        updatedAt: expect.any(String),
 | 
						|
        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 libraryId: string;
 | 
						|
 | 
						|
      beforeEach(async () => {
 | 
						|
        // Create an external library with default settings
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.EXTERNAL });
 | 
						|
 | 
						|
        expect(status).toBe(201);
 | 
						|
 | 
						|
        libraryId = body.id;
 | 
						|
      });
 | 
						|
 | 
						|
      it('should change the library name', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .put(`/library/${libraryId}`)
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ name: 'New Library Name' });
 | 
						|
        expect(status).toBe(200);
 | 
						|
        expect(body).toEqual({
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.EXTERNAL,
 | 
						|
          name: 'New Library Name',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          refreshedAt: null,
 | 
						|
          assetCount: 0,
 | 
						|
          importPaths: [],
 | 
						|
          exclusionPatterns: [],
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      it('should not set an empty name', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .put(`/library/${libraryId}`)
 | 
						|
          .set('Authorization', `Bearer ${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/${libraryId}`)
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ importPaths: ['/path/to/import'] });
 | 
						|
        expect(status).toBe(200);
 | 
						|
        expect(body).toEqual({
 | 
						|
          id: expect.any(String),
 | 
						|
          ownerId: loginResponse.userId,
 | 
						|
          type: LibraryType.EXTERNAL,
 | 
						|
          name: 'New External Library',
 | 
						|
          createdAt: expect.any(String),
 | 
						|
          updatedAt: expect.any(String),
 | 
						|
          refreshedAt: null,
 | 
						|
          assetCount: 0,
 | 
						|
          importPaths: ['/path/to/import'],
 | 
						|
          exclusionPatterns: [],
 | 
						|
        });
 | 
						|
      });
 | 
						|
 | 
						|
      it('should not allow an empty import path', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .put(`/library/${libraryId}`)
 | 
						|
          .set('Authorization', `Bearer ${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/${libraryId}`)
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ exclusionPatterns: [''] });
 | 
						|
        expect(status).toBe(400);
 | 
						|
        expect(body).toEqual(errorStub.badRequest(['each value in exclusionPatterns should not be empty']));
 | 
						|
      });
 | 
						|
 | 
						|
      it('should not allow an empty exclusion pattern', async () => {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .put(`/library/${libraryId}`)
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ importPaths: [''] });
 | 
						|
        expect(status).toBe(400);
 | 
						|
        expect(body).toEqual(errorStub.badRequest(['each value in importPaths 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 () => {
 | 
						|
      let libraryId: string;
 | 
						|
      {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.EXTERNAL });
 | 
						|
        expect(status).toBe(201);
 | 
						|
        libraryId = body.id;
 | 
						|
      }
 | 
						|
      const { status, body } = await request(server)
 | 
						|
        .get(`/library/${libraryId}`)
 | 
						|
        .set('Authorization', `Bearer ${accessToken}`);
 | 
						|
 | 
						|
      expect(status).toBe(200);
 | 
						|
      expect(body).toEqual({
 | 
						|
        id: expect.any(String),
 | 
						|
        ownerId: loginResponse.userId,
 | 
						|
        type: LibraryType.EXTERNAL,
 | 
						|
        name: 'New External Library',
 | 
						|
        createdAt: expect.any(String),
 | 
						|
        updatedAt: expect.any(String),
 | 
						|
        refreshedAt: null,
 | 
						|
        assetCount: 0,
 | 
						|
        importPaths: [],
 | 
						|
        exclusionPatterns: [],
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    it("should not allow getting another user's library", async () => {
 | 
						|
      await api.userCreate(server, accessToken, userStub.user1);
 | 
						|
 | 
						|
      const loginResponse = await api.login(server, {
 | 
						|
        email: userStub.user1.email,
 | 
						|
        password: userStub.user1.password ?? '',
 | 
						|
      });
 | 
						|
 | 
						|
      let libraryId: string;
 | 
						|
      {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.EXTERNAL });
 | 
						|
        expect(status).toBe(201);
 | 
						|
        libraryId = body.id;
 | 
						|
      }
 | 
						|
 | 
						|
      const { status, body } = await request(server)
 | 
						|
        .get(`/library/${libraryId}`)
 | 
						|
        .set('Authorization', `Bearer ${loginResponse.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, accessToken);
 | 
						|
      expect(defaultLibrary).toBeDefined();
 | 
						|
 | 
						|
      const { status, body } = await request(server)
 | 
						|
        .delete(`/library/${defaultLibrary.id}`)
 | 
						|
        .set('Authorization', `Bearer ${accessToken}`);
 | 
						|
      expect(status).toBe(400);
 | 
						|
      expect(body).toEqual(errorStub.noDeleteUploadLibrary);
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  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', async () => {
 | 
						|
      let libraryId: string;
 | 
						|
      {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.EXTERNAL });
 | 
						|
        expect(status).toBe(201);
 | 
						|
        libraryId = body.id;
 | 
						|
      }
 | 
						|
 | 
						|
      const { status, body } = await request(server)
 | 
						|
        .post(`/library/${libraryId}/scan`)
 | 
						|
        .set('Authorization', `Bearer ${accessToken}`);
 | 
						|
 | 
						|
      expect(status).toBe(201);
 | 
						|
      expect(body).toEqual({});
 | 
						|
    });
 | 
						|
 | 
						|
    it('should not scan an upload library', async () => {
 | 
						|
      let libraryId: string;
 | 
						|
      {
 | 
						|
        const { status, body } = await request(server)
 | 
						|
          .post('/library')
 | 
						|
          .set('Authorization', `Bearer ${accessToken}`)
 | 
						|
          .send({ type: LibraryType.UPLOAD });
 | 
						|
        expect(status).toBe(201);
 | 
						|
        libraryId = body.id;
 | 
						|
      }
 | 
						|
 | 
						|
      const { status, body } = await request(server)
 | 
						|
        .post(`/library/${libraryId}/scan`)
 | 
						|
        .set('Authorization', `Bearer ${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);
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |