feat(server): better api error messages (for unhandled exceptions) (#4817)

* feat(server): better error messages

* chore: open api

* chore: remove debug log

* fix: syntax error

* fix: e2e test
This commit is contained in:
Jason Rasmussen
2023-11-03 21:33:15 -04:00
committed by GitHub
parent d4ef6f52bb
commit 2e424fe249
72 changed files with 1974 additions and 1952 deletions
+19 -19
View File
@@ -678,7 +678,7 @@
},
"/api-key": {
"get": {
"operationId": "getKeys",
"operationId": "getApiKeys",
"parameters": [],
"responses": {
"200": {
@@ -711,7 +711,7 @@
]
},
"post": {
"operationId": "createKey",
"operationId": "createApiKey",
"parameters": [],
"requestBody": {
"content": {
@@ -753,7 +753,7 @@
},
"/api-key/{id}": {
"delete": {
"operationId": "deleteKey",
"operationId": "deleteApiKey",
"parameters": [
{
"name": "id",
@@ -786,7 +786,7 @@
]
},
"get": {
"operationId": "getKey",
"operationId": "getApiKey",
"parameters": [
{
"name": "id",
@@ -826,7 +826,7 @@
]
},
"put": {
"operationId": "updateKey",
"operationId": "updateApiKey",
"parameters": [
{
"name": "id",
@@ -1084,7 +1084,7 @@
"/asset/bulk-upload-check": {
"post": {
"description": "Checks if assets exist by checksums",
"operationId": "bulkUploadCheck",
"operationId": "checkBulkUpload",
"parameters": [],
"requestBody": {
"content": {
@@ -1855,7 +1855,7 @@
},
"/asset/statistics": {
"get": {
"operationId": "getAssetStats",
"operationId": "getAssetStatistics",
"parameters": [
{
"name": "isArchived",
@@ -1977,7 +1977,7 @@
},
"/asset/time-bucket": {
"get": {
"operationId": "getByTimeBucket",
"operationId": "getTimeBucket",
"parameters": [
{
"name": "size",
@@ -2596,7 +2596,7 @@
},
"/auth/admin-sign-up": {
"post": {
"operationId": "adminSignUp",
"operationId": "signUpAdmin",
"parameters": [],
"requestBody": {
"content": {
@@ -2943,7 +2943,7 @@
},
"/library": {
"get": {
"operationId": "getAllForUser",
"operationId": "getLibraries",
"parameters": [],
"responses": {
"200": {
@@ -3265,7 +3265,7 @@
},
"/oauth/authorize": {
"post": {
"operationId": "authorizeOAuth",
"operationId": "startOAuth",
"parameters": [],
"requestBody": {
"content": {
@@ -3296,7 +3296,7 @@
},
"/oauth/callback": {
"post": {
"operationId": "callback",
"operationId": "finishOAuth",
"parameters": [],
"requestBody": {
"content": {
@@ -3329,7 +3329,7 @@
"post": {
"deprecated": true,
"description": "@deprecated use feature flags and /oauth/authorize",
"operationId": "generateConfig",
"operationId": "generateOAuthConfig",
"parameters": [],
"requestBody": {
"content": {
@@ -3360,7 +3360,7 @@
},
"/oauth/link": {
"post": {
"operationId": "link",
"operationId": "linkOAuthAccount",
"parameters": [],
"requestBody": {
"content": {
@@ -3402,7 +3402,7 @@
},
"/oauth/mobile-redirect": {
"get": {
"operationId": "mobileRedirect",
"operationId": "redirectOAuthToMobile",
"parameters": [],
"responses": {
"200": {
@@ -3416,7 +3416,7 @@
},
"/oauth/unlink": {
"post": {
"operationId": "unlink",
"operationId": "unlinkOAuthAccount",
"parameters": [],
"responses": {
"201": {
@@ -4307,9 +4307,9 @@
]
}
},
"/server-info/stats": {
"/server-info/statistics": {
"get": {
"operationId": "getStats",
"operationId": "getServerStatistics",
"parameters": [],
"responses": {
"200": {
@@ -4837,7 +4837,7 @@
},
"/system-config/defaults": {
"get": {
"operationId": "getDefaults",
"operationId": "getConfigDefaults",
"parameters": [],
"responses": {
"200": {
+10 -10
View File
@@ -331,17 +331,17 @@ describe(AssetService.name, () => {
});
});
describe('getByTimeBucket', () => {
describe('getTimeBucket', () => {
it('should return the assets for a album time bucket if user has album.read', async () => {
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
assetMock.getByTimeBucket.mockResolvedValue([assetStub.image]);
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
await expect(
sut.getByTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-id');
expect(assetMock.getByTimeBucket).toBeCalledWith('bucket', {
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
albumId: 'album-id',
@@ -349,17 +349,17 @@ describe(AssetService.name, () => {
});
it('should return the assets for a archive time bucket if user has archive.read', async () => {
assetMock.getByTimeBucket.mockResolvedValue([assetStub.image]);
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
await expect(
sut.getByTimeBucket(authStub.admin, {
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
userId: authStub.admin.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(assetMock.getByTimeBucket).toBeCalledWith('bucket', {
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
@@ -368,16 +368,16 @@ describe(AssetService.name, () => {
});
it('should return the assets for a library time bucket if user has library.read', async () => {
assetMock.getByTimeBucket.mockResolvedValue([assetStub.image]);
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
await expect(
sut.getByTimeBucket(authStub.admin, {
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userId: authStub.admin.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
expect(assetMock.getByTimeBucket).toBeCalledWith('bucket', {
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
userId: authStub.admin.id,
+2 -2
View File
@@ -194,12 +194,12 @@ export class AssetService {
return this.assetRepository.getTimeBuckets(dto);
}
async getByTimeBucket(
async getTimeBucket(
authUser: AuthUserDto,
dto: TimeBucketAssetDto,
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
await this.timeBucketChecks(authUser, dto);
const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto);
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, dto);
if (authUser.isShowMetadata) {
return assets.map((asset) => mapAsset(asset, { withStack: true }));
} else {
+2 -7
View File
@@ -315,13 +315,8 @@ export class AuthService {
const redirectUri = this.normalize(config, url.split('?')[0]);
const client = await this.getOAuthClient(config);
const params = client.callbackParams(url);
try {
const tokens = await client.callback(redirectUri, params, { state: params.state });
return client.userinfo<OAuthProfile>(tokens.access_token || '');
} catch (error: Error | any) {
this.logger.error(`Unable to complete OAuth login: ${error}`, error?.stack);
throw new InternalServerErrorException(`Unable to complete OAuth login: ${error}`, { cause: error });
}
const tokens = await client.callback(redirectUri, params, { state: params.state });
return client.userinfo<OAuthProfile>(tokens.access_token || '');
}
private async getOAuthClient(config: SystemConfig) {
@@ -123,6 +123,6 @@ export interface IAssetRepository {
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
}
@@ -220,7 +220,7 @@ describe(ServerInfoService.name, () => {
},
]);
await expect(sut.getStats()).resolves.toEqual({
await expect(sut.getStatistics()).resolves.toEqual({
photos: 120,
videos: 31,
usage: 1123455,
@@ -92,7 +92,7 @@ export class ServerInfoService {
};
}
async getStats(): Promise<ServerStatsResponseDto> {
async getStatistics(): Promise<ServerStatsResponseDto> {
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
const serverStats = new ServerStatsResponseDto();
+34 -44
View File
@@ -1,5 +1,5 @@
import { LibraryType, UserEntity } from '@app/infra/entities';
import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common';
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import path from 'path';
import sanitize from 'sanitize-filename';
import { AuthUserDto } from '../auth';
@@ -61,26 +61,21 @@ export class UserCore {
}
}
try {
if (dto.password) {
dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
}
if (dto.storageLabel === '') {
dto.storageLabel = null;
}
if (dto.externalPath === '') {
dto.externalPath = null;
} else if (dto.externalPath) {
dto.externalPath = path.normalize(dto.externalPath);
}
return this.userRepository.update(id, dto);
} catch (e) {
Logger.error(e, 'Failed to update user info');
throw new InternalServerErrorException('Failed to update user info');
if (dto.password) {
dto.password = await this.cryptoRepository.hashBcrypt(dto.password, SALT_ROUNDS);
}
if (dto.storageLabel === '') {
dto.storageLabel = null;
}
if (dto.externalPath === '') {
dto.externalPath = null;
} else if (dto.externalPath) {
dto.externalPath = path.normalize(dto.externalPath);
}
return this.userRepository.update(id, dto);
}
async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
@@ -96,30 +91,25 @@ export class UserCore {
}
}
try {
const payload: Partial<UserEntity> = { ...dto };
if (payload.password) {
payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
}
if (payload.storageLabel) {
payload.storageLabel = sanitize(payload.storageLabel);
}
const userEntity = await this.userRepository.create(payload);
await this.libraryRepository.create({
owner: { id: userEntity.id } as UserEntity,
name: 'Default Library',
assets: [],
type: LibraryType.UPLOAD,
importPaths: [],
exclusionPatterns: [],
isVisible: true,
});
return userEntity;
} catch (e) {
Logger.error(e, 'Create new user');
throw new InternalServerErrorException('Failed to register new user');
const payload: Partial<UserEntity> = { ...dto };
if (payload.password) {
payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
}
if (payload.storageLabel) {
payload.storageLabel = sanitize(payload.storageLabel);
}
const userEntity = await this.userRepository.create(payload);
await this.libraryRepository.create({
owner: { id: userEntity.id } as UserEntity,
name: 'Default Library',
assets: [],
type: LibraryType.UPLOAD,
importPaths: [],
exclusionPatterns: [],
isVisible: true,
});
return userEntity;
}
}
@@ -17,8 +17,8 @@ import {
import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { Response as Res } from 'express';
import { AuthUser, Authenticated, SharedLinkRoute } from '../../app.guard';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../app.interceptor';
import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from '../../interceptors';
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
import { AssetService } from './asset.service';
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
@@ -204,7 +204,7 @@ export class AssetController {
*/
@Post('/bulk-upload-check')
@HttpCode(HttpStatus.OK)
bulkUploadCheck(
checkBulkUpload(
@AuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
): Promise<AssetBulkUploadCheckResponseDto> {
+4 -5
View File
@@ -2,14 +2,13 @@ import { DomainModule } from '@app/domain';
import { InfraModule } from '@app/infra';
import { AssetEntity } from '@app/infra/entities';
import { Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ScheduleModule } from '@nestjs/schedule';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetRepository, IAssetRepository } from './api-v1/asset/asset-repository';
import { AssetController as AssetControllerV1 } from './api-v1/asset/asset.controller';
import { AssetService } from './api-v1/asset/asset.service';
import { AppGuard } from './app.guard';
import { FileUploadInterceptor } from './app.interceptor';
import { AppService } from './app.service';
import {
APIKeyController,
@@ -31,6 +30,7 @@ import {
TagController,
UserController,
} from './controllers';
import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
@Module({
imports: [
@@ -61,10 +61,9 @@ import {
PersonController,
],
providers: [
//
{ provide: APP_GUARD, useExisting: AppGuard },
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
{ provide: APP_GUARD, useClass: AppGuard },
{ provide: IAssetRepository, useClass: AssetRepository },
AppGuard,
AppService,
AssetService,
FileUploadInterceptor,
+7
View File
@@ -47,6 +47,9 @@ function sortKeys<T>(obj: T): T {
return result as T;
}
export const routeToErrorMessage = (methodName: string) =>
'Failed to ' + methodName.replace(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
const patchOpenAPI = (document: OpenAPIObject) => {
document.paths = sortKeys(document.paths);
if (document.components?.schemas) {
@@ -78,6 +81,10 @@ const patchOpenAPI = (document: OpenAPIObject) => {
delete operation.summary;
}
if (operation.operationId) {
// console.log(`${routeToErrorMessage(operation.operationId).padEnd(40)} (${operation.operationId})`);
}
if (operation.description === '') {
delete operation.description;
}
@@ -20,22 +20,22 @@ export class APIKeyController {
constructor(private service: APIKeyService) {}
@Post()
createKey(@AuthUser() authUser: AuthUserDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
createApiKey(@AuthUser() authUser: AuthUserDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(authUser, dto);
}
@Get()
getKeys(@AuthUser() authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
getApiKeys(@AuthUser() authUser: AuthUserDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(authUser);
}
@Get(':id')
getKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
getApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(authUser, id);
}
@Put(':id')
updateKey(
updateApiKey(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: APIKeyUpdateDto,
@@ -44,7 +44,7 @@ export class APIKeyController {
}
@Delete(':id')
deleteKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
deleteApiKey(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(authUser, id);
}
}
@@ -39,10 +39,11 @@ import {
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard';
import { UseValidation, asStreamableFile } from '../app.utils';
import { Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ApiTags('Asset')
@Controller('asset')
@Controller(Route.ASSET)
@Authenticated()
@UseValidation()
export class AssetController {
@@ -86,7 +87,7 @@ export class AssetController {
}
@Get('statistics')
getAssetStats(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
getAssetStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise<AssetStatsResponseDto> {
return this.service.getStatistics(authUser, dto);
}
@@ -98,8 +99,8 @@ export class AssetController {
@Authenticated({ isShared: true })
@Get('time-bucket')
getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getByTimeBucket(authUser, dto) as Promise<AssetResponseDto[]>;
getTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
return this.service.getTimeBucket(authUser, dto) as Promise<AssetResponseDto[]>;
}
@Post('jobs')
@@ -43,7 +43,7 @@ export class AuthController {
@PublicRoute()
@Post('admin-sign-up')
@ApiBadRequestResponse({ description: 'The server already has an admin' })
adminSignUp(@Body() signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
signUpAdmin(@Body() signUpCredential: SignUpDto): Promise<AdminSignupResponseDto> {
return this.service.adminSignUp(signUpCredential);
}
@@ -21,7 +21,7 @@ export class LibraryController {
constructor(private service: LibraryService) {}
@Get()
getAllForUser(@AuthUser() authUser: AuthUserDto): Promise<ResponseDto[]> {
getLibraries(@AuthUser() authUser: AuthUserDto): Promise<ResponseDto[]> {
return this.service.getAllForUser(authUser);
}
@@ -25,7 +25,7 @@ export class OAuthController {
@PublicRoute()
@Get('mobile-redirect')
@Redirect()
mobileRedirect(@Req() req: Request) {
redirectOAuthToMobile(@Req() req: Request) {
return {
url: this.service.getMobileRedirect(req.url),
statusCode: HttpStatus.TEMPORARY_REDIRECT,
@@ -35,19 +35,19 @@ export class OAuthController {
/** @deprecated use feature flags and /oauth/authorize */
@PublicRoute()
@Post('config')
generateConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
generateOAuthConfig(@Body() dto: OAuthConfigDto): Promise<OAuthConfigResponseDto> {
return this.service.generateConfig(dto);
}
@PublicRoute()
@Post('authorize')
authorizeOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
startOAuth(@Body() dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
return this.service.authorize(dto);
}
@PublicRoute()
@Post('callback')
async callback(
async finishOAuth(
@Res({ passthrough: true }) res: Response,
@Body() dto: OAuthCallbackDto,
@GetLoginDetails() loginDetails: LoginDetails,
@@ -58,12 +58,12 @@ export class OAuthController {
}
@Post('link')
link(@AuthUser() authUser: AuthUserDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
linkOAuthAccount(@AuthUser() authUser: AuthUserDto, @Body() dto: OAuthCallbackDto): Promise<UserResponseDto> {
return this.service.link(authUser, dto);
}
@Post('unlink')
unlink(@AuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
unlinkOAuthAccount(@AuthUser() authUser: AuthUserDto): Promise<UserResponseDto> {
return this.service.unlink(authUser);
}
}
@@ -57,9 +57,9 @@ export class ServerInfoController {
}
@AdminRoute()
@Get('stats')
getStats(): Promise<ServerStatsResponseDto> {
return this.service.getStats();
@Get('statistics')
getServerStatistics(): Promise<ServerStatsResponseDto> {
return this.service.getStatistics();
}
@PublicRoute()
@@ -17,7 +17,7 @@ export class SystemConfigController {
}
@Get('defaults')
getDefaults(): SystemConfigDto {
getConfigDefaults(): SystemConfigDto {
return this.service.getDefaults();
}
@@ -22,8 +22,8 @@ import {
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { AdminRoute, AuthUser, Authenticated } from '../app.guard';
import { FileUploadInterceptor, Route } from '../app.interceptor';
import { UseValidation, asStreamableFile } from '../app.utils';
import { FileUploadInterceptor, Route } from '../interceptors';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ApiTags('User')
@@ -0,0 +1,32 @@
import {
CallHandler,
ExecutionContext,
HttpException,
Injectable,
InternalServerErrorException,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable, catchError, throwError } from 'rxjs';
import { routeToErrorMessage } from '../app.utils';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
private logger = new Logger(ErrorInterceptor.name);
async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
return next.handle().pipe(
catchError((error) =>
throwError(() => {
if (error instanceof HttpException === false) {
const errorMessage = routeToErrorMessage(context.getHandler().name);
this.logger.error(errorMessage, error, error?.errors);
return new InternalServerErrorException(errorMessage);
} else {
return error;
}
}),
),
);
}
}
@@ -7,7 +7,7 @@ import { createHash } from 'crypto';
import { NextFunction, RequestHandler } from 'express';
import multer, { StorageEngine, diskStorage } from 'multer';
import { Observable } from 'rxjs';
import { AuthRequest } from './app.guard';
import { AuthRequest } from '../app.guard';
export enum Route {
ASSET = 'asset',
+2
View File
@@ -0,0 +1,2 @@
export * from './error.interceptor';
export * from './file.interceptor';
@@ -493,7 +493,7 @@ export class AssetRepository implements IAssetRepository {
.getRawMany();
}
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
const truncated = dateTrunc(options);
return (
this.getBuilder(options)
+4 -4
View File
@@ -103,9 +103,9 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
});
});
describe('GET /server-info/stats', () => {
describe('GET /server-info/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/server-info/stats');
const { status, body } = await request(server).get('/server-info/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
@@ -115,7 +115,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
await api.userApi.create(server, accessToken, { ...loginDto, firstName: 'test', lastName: 'test' });
const { accessToken: userAccessToken } = await api.authApi.login(server, loginDto);
const { status, body } = await request(server)
.get('/server-info/stats')
.get('/server-info/statistics')
.set('Authorization', `Bearer ${userAccessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorStub.forbidden);
@@ -123,7 +123,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
it('should return the server stats', async () => {
const { status, body } = await request(server)
.get('/server-info/stats')
.get('/server-info/statistics')
.set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
@@ -26,7 +26,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
findLivePhotoMatch: jest.fn(),
getMapMarkers: jest.fn(),
getStatistics: jest.fn(),
getByTimeBucket: jest.fn(),
getTimeBucket: jest.fn(),
getTimeBuckets: jest.fn(),
restoreAll: jest.fn(),
softDeleteAll: jest.fn(),