mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(server,web): libraries (#3124)
* feat: libraries Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
816db700e1
commit
acdc66413c
143 changed files with 10941 additions and 386 deletions
494
server/test/e2e/library.e2e-spec.ts
Normal file
494
server/test/e2e/library.e2e-spec.ts
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue