refactor: controller tests (#18035)

* feat: controller unit tests

* refactor: controller tests
This commit is contained in:
Jason Rasmussen
2025-05-03 09:39:44 -04:00
committed by GitHub
parent 62fc5b3c7d
commit ea9f11bf39
23 changed files with 1035 additions and 805 deletions

View File

@@ -0,0 +1,81 @@
import { ActivityController } from 'src/controllers/activity.controller';
import { ActivityService } from 'src/services/activity.service';
import request from 'supertest';
import { factory } from 'test/small.factory';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(ActivityController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(ActivityController, [
{ provide: ActivityService, useValue: mockBaseService(ActivityService) },
]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('GET /activities', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/activities');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require an albumId', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/activities');
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid albumId', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' });
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid assetId', async () => {
const { status, body } = await request(ctx.getHttpServer())
.get('/activities')
.query({ albumId: factory.uuid(), assetId: '123' });
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
});
});
describe('POST /activities', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/activities');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require an albumId', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/activities').send({ albumId: '123' });
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should require a comment when type is comment', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/activities')
.send({ albumId: factory.uuid(), type: 'comment', comment: null });
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(['comment must be a string', 'comment should not be empty']));
});
});
describe('DELETE /activities/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).delete(`/activities/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`);
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
});
});
});

View File

@@ -0,0 +1,86 @@
import { AlbumController } from 'src/controllers/album.controller';
import { AlbumService } from 'src/services/album.service';
import request from 'supertest';
import { factory } from 'test/small.factory';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(AlbumController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(AlbumController, [{ provide: AlbumService, useValue: mockBaseService(AlbumService) }]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('GET /albums', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/albums');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should reject an invalid shared param', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid');
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(['shared must be a boolean value']));
});
it('should reject an invalid assetId param', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid');
expect(status).toEqual(400);
expect(body).toEqual(factory.responses.badRequest(['assetId must be a UUID']));
});
});
describe('GET /albums/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/albums/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('GET /albums/statistics', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/albums/statistics');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('POST /albums', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/albums').send({ albumName: 'New album' });
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('PUT /albums/:id/assets', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/assets`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('PATCH /albums/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).patch(`/albums/${factory.uuid()}`).send({ albumName: 'New album name' });
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('DELETE /albums/:id/assets', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).delete(`/albums/${factory.uuid()}/assets`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('PUT :id/users', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put(`/albums/${factory.uuid()}/users`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,73 @@
import { APIKeyController } from 'src/controllers/api-key.controller';
import { ApiKeyService } from 'src/services/api-key.service';
import request from 'supertest';
import { factory } from 'test/small.factory';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(APIKeyController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(APIKeyController, [
{ provide: ApiKeyService, useValue: mockBaseService(ApiKeyService) },
]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('POST /api-keys', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/api-keys').send({ name: 'API Key' });
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('GET /api-keys', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/api-keys');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('GET /api-keys/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/api-keys/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`);
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
});
});
describe('PUT /api-keys/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put(`/api-keys/${factory.uuid()}`).send({ name: 'new name' });
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/api-keys/123`).send({ name: 'new name' });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
});
});
describe('DELETE /api-keys/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).delete(`/api-keys/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`);
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
});
});
});

View File

@@ -0,0 +1,49 @@
import { AppController } from 'src/controllers/app.controller';
import { SystemConfigService } from 'src/services/system-config.service';
import request from 'supertest';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(AppController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(AppController, [
{ provide: SystemConfigService, useValue: mockBaseService(SystemConfigService) },
]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('GET /.well-known/immich', () => {
it('should not be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/.well-known/immich');
expect(ctx.authenticate).not.toHaveBeenCalled();
});
it('should return a 200 status code', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/.well-known/immich');
expect(status).toBe(200);
expect(body).toEqual({
api: {
endpoint: '/api',
},
});
});
});
describe('GET /custom.css', () => {
it('should not be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/custom.css');
expect(ctx.authenticate).not.toHaveBeenCalled();
});
it('should reply with text/css', async () => {
const { status, headers } = await request(ctx.getHttpServer()).get('/custom.css');
expect(status).toBe(200);
expect(headers['content-type']).toEqual('text/css; charset=utf-8');
});
});
});

View File

@@ -0,0 +1,137 @@
import { AssetMediaController } from 'src/controllers/asset-media.controller';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { AssetMediaService } from 'src/services/asset-media.service';
import request from 'supertest';
import { factory } from 'test/small.factory';
import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
const dto: Record<string, any> = {
deviceAssetId: 'example-image',
deviceId: 'TEST',
fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(),
isFavorite: 'testing',
duration: '0:00:00.000000',
};
const omit = options?.omit;
if (omit) {
delete dto[omit];
}
return dto;
};
describe(AssetMediaController.name, () => {
let ctx: ControllerContext;
const assetData = Buffer.from('123');
const filename = 'example.png';
beforeAll(async () => {
ctx = await controllerSetup(AssetMediaController, [
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
{ provide: AssetMediaService, useValue: mockBaseService(AssetMediaService) },
]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('POST /assets', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post(`/assets`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require `deviceAssetId`', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto({ omit: 'deviceAssetId' }) });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
it('should require `deviceId`', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto({ omit: 'deviceId' }) });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
it('should require `fileCreatedAt`', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
it('should require `fileModifiedAt`', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto({ omit: 'fileModifiedAt' }) });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
it('should require `duration`', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto({ omit: 'duration' }) });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
it('should throw if `isFavorite` is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
it('should throw if `isVisible` is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto(), isVisible: 'not-a-boolean' });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
it('should throw if `isArchived` is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto(), isArchived: 'not-a-boolean' });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
});
// TODO figure out how to deal with `sendFile`
describe.skip('GET /assets/:id/original', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
// TODO figure out how to deal with `sendFile`
describe.skip('GET /assets/:id/thumbnail', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,118 @@
import { AssetController } from 'src/controllers/asset.controller';
import { AssetService } from 'src/services/asset.service';
import request from 'supertest';
import { factory } from 'test/small.factory';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(AssetController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(AssetController, [{ provide: AssetService, useValue: mockBaseService(AssetService) }]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('PUT /assets', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put(`/assets`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('DELETE /assets', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer())
.delete(`/assets`)
.send({ ids: [factory.uuid()] });
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer())
.delete(`/assets`)
.send({ ids: ['123'] });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID']));
});
});
describe('GET /assets/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`);
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
});
});
describe('PUT /assets/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/123`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`);
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
});
it('should reject invalid gps coordinates', async () => {
for (const test of [
{ latitude: 12 },
{ longitude: 12 },
{ latitude: 12, longitude: 'abc' },
{ latitude: 'abc', longitude: 12 },
{ latitude: null, longitude: 12 },
{ latitude: 12, longitude: null },
{ latitude: 91, longitude: 12 },
{ latitude: -91, longitude: 12 },
{ latitude: 12, longitude: -181 },
{ latitude: 12, longitude: 181 },
]) {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
}
});
it('should reject invalid rating', async () => {
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
}
});
});
describe('GET /assets/statistics', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/statistics`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('GET /assets/random', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/random`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should not allow count to be a string', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/assets/random?count=ABC');
expect(status).toBe(400);
expect(body).toEqual(
factory.responses.badRequest(['count must be a positive number', 'count must be an integer number']),
);
});
});
});

View File

@@ -0,0 +1,60 @@
import { AuthController } from 'src/controllers/auth.controller';
import { AuthService } from 'src/services/auth.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(AuthController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(AuthService);
beforeAll(async () => {
ctx = await controllerSetup(AuthController, [{ provide: AuthService, useValue: service }]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('POST /auth/admin-sign-up', () => {
const name = 'admin';
const email = 'admin@immich.cloud';
const password = 'password';
it('should require an email address', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password });
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require a password', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email });
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require a name', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password });
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require a valid email', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/auth/admin-sign-up')
.send({ name, email: 'immich', password });
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should transform email to lower case', async () => {
service.adminSignUp.mockReset();
const { status } = await request(ctx.getHttpServer())
.post('/auth/admin-sign-up')
.send({ name: 'admin', password: 'password', email: 'aDmIn@IMMICH.cloud' });
expect(status).toEqual(201);
expect(service.adminSignUp).toHaveBeenCalledWith(expect.objectContaining({ email: 'admin@immich.cloud' }));
});
});
});

View File

@@ -0,0 +1,64 @@
import { NotificationController } from 'src/controllers/notification.controller';
import { NotificationService } from 'src/services/notification.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { factory } from 'test/small.factory';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(NotificationController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(NotificationController, [
{ provide: NotificationService, useValue: mockBaseService(NotificationService) },
]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('GET /notifications', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/notifications');
expect(ctx.authenticate).toHaveBeenCalled();
});
it(`should reject an invalid notification level`, async () => {
const { status, body } = await request(ctx.getHttpServer())
.get(`/notifications`)
.query({ level: 'invalid' })
.set('Authorization', `Bearer token`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')]));
});
});
describe('PUT /notifications', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/notifications');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('GET /notifications/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/notifications/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')]));
});
});
describe('PUT /notifications/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put(`/notifications/${factory.uuid()}`).send({ readAt: factory.date() });
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,201 @@
import { SearchController } from 'src/controllers/search.controller';
import { SearchService } from 'src/services/search.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(SearchController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(SearchController, [
{ provide: SearchService, useValue: mockBaseService(SearchService) },
]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('POST /search/metadata', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/search/metadata');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should reject page as a string', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 'abc' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['page must not be less than 1', 'page must be an integer number']));
});
it('should reject page as a negative number', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: -10 });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['page must not be less than 1']));
});
it('should reject page as 0', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 0 });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['page must not be less than 1']));
});
it('should reject size as a string', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: 'abc' });
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest([
'size must not be greater than 1000',
'size must not be less than 1',
'size must be an integer number',
]),
);
});
it('should reject an invalid size', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: -1.5 });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['size must not be less than 1', 'size must be an integer number']));
});
it('should reject an isArchived as not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata')
.send({ isArchived: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['isArchived must be a boolean value']));
});
it('should reject an isFavorite as not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata')
.send({ isFavorite: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value']));
});
it('should reject an isEncoded as not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata')
.send({ isEncoded: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['isEncoded must be a boolean value']));
});
it('should reject an isOffline as not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata')
.send({ isOffline: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['isOffline must be a boolean value']));
});
it('should reject an isMotion as not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ isMotion: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value']));
});
it('should reject an isVisible as not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata')
.send({ isVisible: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['isVisible must be a boolean value']));
});
});
describe('POST /search/random', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/search/random');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should reject if withStacked is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/random')
.send({ withStacked: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value']));
});
it('should reject if withPeople is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/random').send({ withPeople: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value']));
});
});
describe('POST /search/smart', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/search/smart');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a query', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string']));
});
});
describe('GET /search/explore', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/explore');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('POST /search/person', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/person');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a name', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
});
});
describe('GET /search/places', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/places');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a name', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
});
});
describe('GET /search/cities', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/cities');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('GET /search/suggestions', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/suggestions');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a type', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest([
'type should not be empty',
expect.stringContaining('type must be one of the following values:'),
]),
);
});
});
});

View File

@@ -0,0 +1,28 @@
import { ServerController } from 'src/controllers/server.controller';
import { ServerService } from 'src/services/server.service';
import { VersionService } from 'src/services/version.service';
import request from 'supertest';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(ServerController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(ServerController, [
{ provide: ServerService, useValue: mockBaseService(ServerService) },
{ provide: VersionService, useValue: mockBaseService(VersionService) },
]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('GET /server/license', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/server/license');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,77 @@
import { UserController } from 'src/controllers/user.controller';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { UserService } from 'src/services/user.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { factory } from 'test/small.factory';
import { automock, ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(UserController.name, () => {
let ctx: ControllerContext;
beforeAll(async () => {
ctx = await controllerSetup(UserController, [
{ provide: LoggingRepository, useValue: automock(LoggingRepository, { strict: false }) },
{ provide: UserService, useValue: mockBaseService(UserService) },
]);
return () => ctx.close();
});
beforeEach(() => {
ctx.reset();
});
describe('GET /users', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/users');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('GET /users/me', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/users/me');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('PUT /users/me', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put('/users/me');
expect(ctx.authenticate).toHaveBeenCalled();
});
for (const key of ['email', 'name']) {
it(`should not allow null ${key}`, async () => {
const dto = { [key]: null };
const { status, body } = await request(ctx.getHttpServer())
.put(`/users/me`)
.set('Authorization', `Bearer token`)
.send(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
});
describe('GET /users/:id', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/users/${factory.uuid()}`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('PUT /users/me/license', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).put('/users/me/license');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('DELETE /users/me/license', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).delete('/users/me/license');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});