mirror of
https://github.com/immich-app/immich
synced 2026-08-22 13:13:05 +00:00
refactor: library controller tests (#30863)
This commit is contained in:
parent
2018e4f8c4
commit
db3b31709d
2 changed files with 203 additions and 173 deletions
|
|
@ -1,8 +1,6 @@
|
|||
import { LibraryResponseDto, LoginResponseDto, getAllLibraries } from '@immich/sdk';
|
||||
import { cpSync, existsSync } from 'node:fs';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { userDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { utimes } from 'utimes';
|
||||
|
|
@ -10,7 +8,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
|||
|
||||
describe('/libraries', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user: LoginResponseDto;
|
||||
let library: LibraryResponseDto;
|
||||
let websocket: Socket;
|
||||
|
||||
|
|
@ -18,7 +15,6 @@ describe('/libraries', () => {
|
|||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
await utils.resetAdminConfig(admin.accessToken);
|
||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
||||
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
|
||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
|
||||
|
|
@ -34,31 +30,7 @@ describe('/libraries', () => {
|
|||
utils.resetEvents();
|
||||
});
|
||||
|
||||
describe('GET /libraries', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/libraries');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /libraries', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/libraries').send({});
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require admin authentication', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/libraries')
|
||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||
.send({ ownerId: admin.userId });
|
||||
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should create an external library with defaults', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/libraries')
|
||||
|
|
@ -97,49 +69,9 @@ describe('/libraries', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not create an external library with duplicate import paths', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/libraries')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({
|
||||
ownerId: admin.userId,
|
||||
name: 'My Awesome Library',
|
||||
importPaths: ['/path', '/path'],
|
||||
exclusionPatterns: ['**/Raw/**'],
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not create an external library with duplicate exclusion patterns', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/libraries')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({
|
||||
ownerId: admin.userId,
|
||||
name: 'My Awesome Library',
|
||||
importPaths: ['/path/to/import'],
|
||||
exclusionPatterns: ['**/Raw/**', '**/Raw/**'],
|
||||
});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /libraries/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put(`/libraries/${uuidDto.notFound}`).send({});
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should change the library name', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/libraries/${library.id}`)
|
||||
|
|
@ -154,18 +86,6 @@ describe('/libraries', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should not set an empty name', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/libraries/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ name: '' });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['name'], message: 'Too small: expected string to have >=1 characters' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should change the import paths', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/libraries/${library.id}`)
|
||||
|
|
@ -180,30 +100,6 @@ describe('/libraries', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should reject an empty import path', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/libraries/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ importPaths: [''] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['importPaths'], message: 'Array items must not be empty' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject duplicate import paths', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/libraries/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ importPaths: ['/path', '/path'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should change the exclusion pattern', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/libraries/${library.id}`)
|
||||
|
|
@ -217,48 +113,9 @@ describe('/libraries', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject duplicate exclusion patterns', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/libraries/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an empty exclusion pattern', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/libraries/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ exclusionPatterns: [''] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array items must not be empty' }]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /libraries/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should require admin access', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/libraries/${uuidDto.notFound}`)
|
||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should get library by id', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
|
||||
|
||||
|
|
@ -280,23 +137,7 @@ describe('/libraries', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('GET /libraries/:id/statistics', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}/statistics`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /libraries/:id/scan', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/scan`).send({});
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should import new asset when scanning external library', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
|
|
@ -785,13 +626,6 @@ describe('/libraries', () => {
|
|||
});
|
||||
|
||||
describe('POST /libraries/:id/validate', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/validate`).send({});
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should pass with no import paths', async () => {
|
||||
const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [] });
|
||||
expect(response.importPaths).toEqual([]);
|
||||
|
|
@ -856,13 +690,6 @@ describe('/libraries', () => {
|
|||
});
|
||||
|
||||
describe('DELETE /libraries/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/libraries/${uuidDto.notFound}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should delete an external library', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
|
||||
|
||||
|
|
|
|||
203
server/src/controllers/library.controller.spec.ts
Normal file
203
server/src/controllers/library.controller.spec.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { LibraryController } from 'src/controllers/library.controller';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import request from 'supertest';
|
||||
import { factory } from 'test/small.factory';
|
||||
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
|
||||
|
||||
describe(LibraryController.name, () => {
|
||||
let ctx: ControllerContext;
|
||||
const service = mockBaseService(LibraryService);
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await controllerSetup(LibraryController, [{ provide: LibraryService, useValue: service }]);
|
||||
return () => ctx.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service.resetAllMocks();
|
||||
ctx.reset();
|
||||
});
|
||||
|
||||
const id = factory.uuid();
|
||||
|
||||
describe('authentication', () => {
|
||||
const routes = [
|
||||
{ method: 'get', path: '/libraries' },
|
||||
{ method: 'post', path: '/libraries' },
|
||||
{ method: 'get', path: `/libraries/${id}` },
|
||||
{ method: 'put', path: `/libraries/${id}` },
|
||||
{ method: 'patch', path: `/libraries/${id}` },
|
||||
{ method: 'delete', path: `/libraries/${id}` },
|
||||
{ method: 'post', path: `/libraries/${id}/validate` },
|
||||
{ method: 'get', path: `/libraries/${id}/statistics` },
|
||||
{ method: 'post', path: `/libraries/${id}/scan` },
|
||||
] as const;
|
||||
|
||||
it.each(routes)('$method $path should be an admin route', async ({ method, path }) => {
|
||||
await request(ctx.getHttpServer())[method](path).send({});
|
||||
|
||||
expect(ctx.authenticate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ metadata: expect.objectContaining({ adminRoute: true }) }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /libraries', () => {
|
||||
it('should require an owner id', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/libraries').send({});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([
|
||||
{ path: ['ownerId'], message: 'Invalid input: expected string, received undefined' },
|
||||
]),
|
||||
);
|
||||
expect(service.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject duplicate import paths', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/libraries')
|
||||
.send({ ownerId: id, importPaths: ['/path', '/path'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
|
||||
);
|
||||
expect(service.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject duplicate exclusion patterns', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.post('/libraries')
|
||||
.send({ ownerId: id, exclusionPatterns: ['**/Raw/**', '**/Raw/**'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
|
||||
);
|
||||
expect(service.create).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /libraries/:id', () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put('/libraries/invalid')
|
||||
.send({ name: 'New Library Name' });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
expect(service.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject an empty name', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/libraries/${id}`).send({ name: '' });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([
|
||||
{ path: ['name'], message: 'Too small: expected string to have >=1 characters' },
|
||||
]),
|
||||
);
|
||||
expect(service.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject an empty import path', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/libraries/${id}`)
|
||||
.send({ importPaths: [''] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([{ path: ['importPaths'], message: 'Array items must not be empty' }]),
|
||||
);
|
||||
expect(service.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject duplicate import paths', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/libraries/${id}`)
|
||||
.send({ importPaths: ['/path', '/path'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
|
||||
);
|
||||
expect(service.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject an empty exclusion pattern', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/libraries/${id}`)
|
||||
.send({ exclusionPatterns: [''] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([{ path: ['exclusionPatterns'], message: 'Array items must not be empty' }]),
|
||||
);
|
||||
expect(service.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject duplicate exclusion patterns', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer())
|
||||
.put(`/libraries/${id}`)
|
||||
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(
|
||||
factory.responses.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
|
||||
);
|
||||
expect(service.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /libraries/:id/validate', () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/libraries/invalid/validate').send({});
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
expect(service.validate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not require import paths', async () => {
|
||||
service.validate.mockResolvedValue({ importPaths: [] });
|
||||
|
||||
const { status } = await request(ctx.getHttpServer()).post(`/libraries/${id}/validate`).send({});
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(service.validate).toHaveBeenCalledWith(id, {});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /libraries/:id/statistics', () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).get('/libraries/invalid/statistics');
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
expect(service.getStatistics).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /libraries/:id/scan', () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).post('/libraries/invalid/scan');
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
expect(service.queueScan).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /libraries/:id', () => {
|
||||
it('should require a valid uuid', async () => {
|
||||
const { status, body } = await request(ctx.getHttpServer()).delete('/libraries/invalid');
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||
expect(service.delete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue