diff --git a/packages/apps/job-launcher/client/src/components/CreditCard/DeleteCardModal.tsx b/packages/apps/job-launcher/client/src/components/CreditCard/DeleteCardModal.tsx index 2255f2c6e3..fe301cb1b8 100644 --- a/packages/apps/job-launcher/client/src/components/CreditCard/DeleteCardModal.tsx +++ b/packages/apps/job-launcher/client/src/components/CreditCard/DeleteCardModal.tsx @@ -43,7 +43,7 @@ const DeleteCardModal = ({ onSuccess(); } catch (error: any) { if ( - error.response?.status === 400 && + error.response?.status === 409 && error.response?.data?.message === 'Cannot delete the default payment method in use' ) { diff --git a/packages/apps/job-launcher/server/src/common/constants/errors.ts b/packages/apps/job-launcher/server/src/common/constants/errors.ts index 68ce51748f..e7b9330205 100644 --- a/packages/apps/job-launcher/server/src/common/constants/errors.ts +++ b/packages/apps/job-launcher/server/src/common/constants/errors.ts @@ -72,6 +72,7 @@ export enum ErrorEscrow { */ export enum ErrorUser { NotFound = 'User not found.', + InvalidStatus = 'User has an invalid status.', AccountCannotBeRegistered = 'Account cannot be registered.', InvalidCredentials = 'Invalid credentials.', UserNotActive = 'User not active.', @@ -216,3 +217,13 @@ export enum ErrorQualification { export enum ErrorEncryption { MissingPrivateKey = 'Encryption private key cannot be empty, when it is enabled', } + +/** + * Represents error messages associated to storage. + */ +export enum ErrorStorage { + FailedToDownload = 'Failed to download file', + NotFound = 'File not found', + InvalidUrl = 'Invalid file URL', + FileNotUploaded = 'File not uploaded', +} diff --git a/packages/apps/job-launcher/server/src/common/errors/base.ts b/packages/apps/job-launcher/server/src/common/errors/base.ts deleted file mode 100644 index dfc528a73b..0000000000 --- a/packages/apps/job-launcher/server/src/common/errors/base.ts +++ /dev/null @@ -1,11 +0,0 @@ -export class BaseError extends Error { - constructor(message: string, cause?: unknown) { - const errorOptions: ErrorOptions = {}; - if (cause) { - errorOptions.cause = cause; - } - - super(message, errorOptions); - this.name = this.constructor.name; - } -} diff --git a/packages/apps/job-launcher/server/src/common/errors/controlled.ts b/packages/apps/job-launcher/server/src/common/errors/controlled.ts deleted file mode 100644 index 32cc401c90..0000000000 --- a/packages/apps/job-launcher/server/src/common/errors/controlled.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HttpStatus } from '@nestjs/common'; - -export class ControlledError extends Error { - status: HttpStatus; - - constructor(message: string, status: HttpStatus, stack?: string) { - super(message); - this.name = this.constructor.name; - this.status = status; - if (stack) this.stack = stack; - else Error.captureStackTrace(this, this.constructor); - } -} diff --git a/packages/apps/job-launcher/server/src/common/errors/database.ts b/packages/apps/job-launcher/server/src/common/errors/database.ts index c369f38558..de5201b9e1 100644 --- a/packages/apps/job-launcher/server/src/common/errors/database.ts +++ b/packages/apps/job-launcher/server/src/common/errors/database.ts @@ -1,13 +1,7 @@ import { QueryFailedError } from 'typeorm'; +import { DatabaseError } from '.'; import { PostgresErrorCodes } from '../enums/database'; -export class DatabaseError extends Error { - constructor(message: string, stack: string) { - super(message); - this.stack = stack; - } -} - export function handleQueryFailedError(error: QueryFailedError): DatabaseError { const stack = error.stack || ''; let message = error.message; diff --git a/packages/apps/job-launcher/server/src/common/errors/index.ts b/packages/apps/job-launcher/server/src/common/errors/index.ts new file mode 100644 index 0000000000..6747ce6861 --- /dev/null +++ b/packages/apps/job-launcher/server/src/common/errors/index.ts @@ -0,0 +1,53 @@ +export * from './database'; + +export class BaseError extends Error { + constructor(message: string, stack?: string) { + super(message); + this.name = this.constructor.name; + if (stack) { + this.stack = stack; + } + } +} + +export class ValidationError extends BaseError { + constructor(message: string, stack?: string) { + super(message, stack); + } +} + +export class AuthError extends BaseError { + constructor(message: string, stack?: string) { + super(message, stack); + } +} + +export class ForbiddenError extends BaseError { + constructor(message: string, stack?: string) { + super(message, stack); + } +} + +export class NotFoundError extends BaseError { + constructor(message: string, stack?: string) { + super(message, stack); + } +} + +export class ConflictError extends BaseError { + constructor(message: string, stack?: string) { + super(message, stack); + } +} + +export class ServerError extends BaseError { + constructor(message: string, stack?: string) { + super(message, stack); + } +} + +export class DatabaseError extends BaseError { + constructor(message: string, stack?: string) { + super(message, stack); + } +} diff --git a/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts b/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts index da42ac85c2..7ad8be772a 100644 --- a/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts +++ b/packages/apps/job-launcher/server/src/common/exceptions/exception.filter.ts @@ -6,44 +6,53 @@ import { Logger, } from '@nestjs/common'; import { Request, Response } from 'express'; -import { DatabaseError } from '../errors/database'; -import { ControlledError } from '../errors/controlled'; +import { + ValidationError, + AuthError, + ForbiddenError, + NotFoundError, + ConflictError, + ServerError, + DatabaseError, +} from '../errors'; @Catch() export class ExceptionFilter implements IExceptionFilter { private logger = new Logger(ExceptionFilter.name); + private getStatus(exception: any): number { + if (exception instanceof ValidationError) { + return HttpStatus.BAD_REQUEST; + } else if (exception instanceof AuthError) { + return HttpStatus.UNAUTHORIZED; + } else if (exception instanceof ForbiddenError) { + return HttpStatus.FORBIDDEN; + } else if (exception instanceof NotFoundError) { + return HttpStatus.NOT_FOUND; + } else if (exception instanceof ConflictError) { + return HttpStatus.CONFLICT; + } else if (exception instanceof ServerError) { + return HttpStatus.UNPROCESSABLE_ENTITY; + } else if (exception instanceof DatabaseError) { + return HttpStatus.UNPROCESSABLE_ENTITY; + } else if (exception.statusCode) { + return exception.statusCode; + } + return HttpStatus.INTERNAL_SERVER_ERROR; + } + catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); - let status = HttpStatus.INTERNAL_SERVER_ERROR; - let message = 'Internal server error'; + const status = this.getStatus(exception); + const message = exception.message || 'Internal server error'; - if (exception instanceof ControlledError) { - status = exception.status; - message = exception.message; - - this.logger.error(`Job Launcher error: ${message}`, exception.stack); - } else if (exception instanceof DatabaseError) { - status = HttpStatus.UNPROCESSABLE_ENTITY; - message = exception.message; - - this.logger.error( - `Database error: ${exception.message}`, - exception.stack, - ); - } else { - if (exception.statusCode === HttpStatus.BAD_REQUEST) { - status = exception.statusCode; - message = exception.message; - } - this.logger.error( - `Unhandled exception: ${exception.message}`, - exception.stack, - ); - } + this.logger.error( + `Exception caught: ${message}`, + exception.stack || 'No stack trace available', + ); response.status(status).json({ statusCode: status, diff --git a/packages/apps/job-launcher/server/src/common/guards/apikey.auth.spec.ts b/packages/apps/job-launcher/server/src/common/guards/apikey.auth.spec.ts index 2e0096d572..24f75dfd1c 100644 --- a/packages/apps/job-launcher/server/src/common/guards/apikey.auth.spec.ts +++ b/packages/apps/job-launcher/server/src/common/guards/apikey.auth.spec.ts @@ -1,9 +1,9 @@ -import { ExecutionContext, HttpStatus } from '@nestjs/common'; +import { ExecutionContext } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiKeyGuard } from './apikey.auth'; import { AuthService } from '../../modules/auth/auth.service'; import { UserEntity } from '../../modules/user/user.entity'; -import { ControlledError } from '../errors/controlled'; +import { AuthError } from '../errors'; +import { ApiKeyGuard } from './apikey.auth'; describe('ApiKeyGuard', () => { let guard: ApiKeyGuard; @@ -71,7 +71,7 @@ describe('ApiKeyGuard', () => { .mockResolvedValue(null); await expect(guard.canActivate(context)).rejects.toThrow( - new ControlledError('Unauthorized', HttpStatus.UNAUTHORIZED), + new AuthError('Unauthorized'), ); }); diff --git a/packages/apps/job-launcher/server/src/common/guards/apikey.auth.ts b/packages/apps/job-launcher/server/src/common/guards/apikey.auth.ts index adec8982a3..8ce67f72fd 100644 --- a/packages/apps/job-launcher/server/src/common/guards/apikey.auth.ts +++ b/packages/apps/job-launcher/server/src/common/guards/apikey.auth.ts @@ -1,12 +1,7 @@ -import { - Injectable, - ExecutionContext, - CanActivate, - HttpStatus, -} from '@nestjs/common'; +import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AuthService } from '../../modules/auth/auth.service'; -import { ControlledError } from '../errors/controlled'; +import { AuthError } from '../errors'; @Injectable() export class ApiKeyGuard implements CanActivate { @@ -35,9 +30,9 @@ export class ApiKeyGuard implements CanActivate { return true; } } else { - throw new ControlledError('Invalid API Key', HttpStatus.UNAUTHORIZED); + throw new AuthError('Invalid API Key'); } } - throw new ControlledError('Unauthorized', HttpStatus.UNAUTHORIZED); + throw new AuthError('Unauthorized'); } } diff --git a/packages/apps/job-launcher/server/src/common/guards/jwt.auth.ts b/packages/apps/job-launcher/server/src/common/guards/jwt.auth.ts index 00176a1008..e60297c4fa 100644 --- a/packages/apps/job-launcher/server/src/common/guards/jwt.auth.ts +++ b/packages/apps/job-launcher/server/src/common/guards/jwt.auth.ts @@ -6,8 +6,8 @@ import { } from '@nestjs/common'; import { ModuleRef, Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; +import { AuthError, ForbiddenError } from '../errors'; import { ApiKeyGuard } from './apikey.auth'; -import { ControlledError } from '../errors/controlled'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt-http') implements CanActivate { @@ -33,7 +33,7 @@ export class JwtAuthGuard extends AuthGuard('jwt-http') implements CanActivate { } } - throw new ControlledError('Unauthorized', HttpStatus.UNAUTHORIZED); + throw new AuthError('Unauthorized'); } public async canActivate(context: ExecutionContext): Promise { @@ -58,11 +58,11 @@ export class JwtAuthGuard extends AuthGuard('jwt-http') implements CanActivate { return this.handleApiKeyAuthentication(context); case HttpStatus.FORBIDDEN: if (jwtError?.response?.message === 'Forbidden') { - throw new ControlledError('Forbidden', HttpStatus.FORBIDDEN); + throw new ForbiddenError('Forbidden'); } break; default: - throw new ControlledError('Unauthorized', HttpStatus.UNAUTHORIZED); + throw new AuthError('Unauthorized'); } return false; diff --git a/packages/apps/job-launcher/server/src/common/guards/signature.auth.spec.ts b/packages/apps/job-launcher/server/src/common/guards/signature.auth.spec.ts index e04d70880c..a3eab12d9e 100644 --- a/packages/apps/job-launcher/server/src/common/guards/signature.auth.spec.ts +++ b/packages/apps/job-launcher/server/src/common/guards/signature.auth.spec.ts @@ -1,11 +1,11 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ExecutionContext, HttpStatus } from '@nestjs/common'; -import { SignatureAuthGuard } from './signature.auth'; -import { verifySignature } from '../utils/signature'; import { ChainId, EscrowUtils } from '@human-protocol/sdk'; +import { ExecutionContext } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; import { MOCK_ADDRESS } from '../../../test/constants'; import { Role } from '../enums/role'; -import { ControlledError } from '../errors/controlled'; +import { verifySignature } from '../utils/signature'; +import { SignatureAuthGuard } from './signature.auth'; +import { AuthError } from '../errors'; jest.mock('../../common/utils/signature'); @@ -82,14 +82,14 @@ describe('SignatureAuthGuard', () => { (verifySignature as jest.Mock).mockReturnValue(false); await expect(guard.canActivate(context as any)).rejects.toThrow( - new ControlledError('Unauthorized', HttpStatus.UNAUTHORIZED), + new AuthError('Unauthorized'), ); }); it('should throw unauthorized exception for unrecognized oracle type', async () => { mockRequest.originalUrl = '/some/random/path'; await expect(guard.canActivate(context as any)).rejects.toThrow( - new ControlledError('Unauthorized', HttpStatus.UNAUTHORIZED), + new AuthError('Unauthorized'), ); }); }); diff --git a/packages/apps/job-launcher/server/src/common/guards/signature.auth.ts b/packages/apps/job-launcher/server/src/common/guards/signature.auth.ts index 9374641e5b..8b536358f5 100644 --- a/packages/apps/job-launcher/server/src/common/guards/signature.auth.ts +++ b/packages/apps/job-launcher/server/src/common/guards/signature.auth.ts @@ -1,17 +1,19 @@ +import { EscrowUtils } from '@human-protocol/sdk'; import { CanActivate, ExecutionContext, - HttpStatus, Injectable, + Logger, } from '@nestjs/common'; -import { verifySignature } from '../utils/signature'; import { HEADER_SIGNATURE_KEY } from '../constants'; -import { EscrowUtils } from '@human-protocol/sdk'; import { Role } from '../enums/role'; -import { ControlledError } from '../errors/controlled'; +import { AuthError } from '../errors'; +import { verifySignature } from '../utils/signature'; @Injectable() export class SignatureAuthGuard implements CanActivate { + private readonly logger = new Logger(SignatureAuthGuard.name); + constructor(private role: Role[]) {} public async canActivate(context: ExecutionContext): Promise { @@ -47,9 +49,12 @@ export class SignatureAuthGuard implements CanActivate { return true; } } catch (error) { - console.error(error); + this.logger.error( + `Error verifying signature: ${error.message}`, + error.stack, + ); } - throw new ControlledError('Unauthorized', HttpStatus.UNAUTHORIZED); + throw new AuthError('Unauthorized'); } } diff --git a/packages/apps/job-launcher/server/src/common/guards/whitelist.auth.spec.ts b/packages/apps/job-launcher/server/src/common/guards/whitelist.auth.spec.ts index d2f686626f..180886a8a8 100644 --- a/packages/apps/job-launcher/server/src/common/guards/whitelist.auth.spec.ts +++ b/packages/apps/job-launcher/server/src/common/guards/whitelist.auth.spec.ts @@ -1,8 +1,8 @@ -import { ExecutionContext, HttpStatus } from '@nestjs/common'; +import { ExecutionContext } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { WhitelistAuthGuard } from './whitelist.auth'; import { WhitelistService } from '../../modules/whitelist/whitelist.service'; -import { ControlledError } from '../errors/controlled'; +import { AuthError } from '../errors'; +import { WhitelistAuthGuard } from './whitelist.auth'; describe('WhitelistAuthGuard', () => { let guard: WhitelistAuthGuard; @@ -36,9 +36,7 @@ describe('WhitelistAuthGuard', () => { await expect( guard.canActivate(mockContext as ExecutionContext), - ).rejects.toThrow( - new ControlledError('User not found.', HttpStatus.UNAUTHORIZED), - ); + ).rejects.toThrow(new AuthError('User not found.')); }); it('should throw an error if the user is not whitelisted', async () => { @@ -54,9 +52,7 @@ describe('WhitelistAuthGuard', () => { await expect( guard.canActivate(mockContext as ExecutionContext), - ).rejects.toThrow( - new ControlledError('Unauthorized.', HttpStatus.UNAUTHORIZED), - ); + ).rejects.toThrow(new AuthError('Unauthorized.')); }); it('should return true if the user is whitelisted', async () => { diff --git a/packages/apps/job-launcher/server/src/common/guards/whitelist.auth.ts b/packages/apps/job-launcher/server/src/common/guards/whitelist.auth.ts index 58e2ccc3ab..5683a2b81f 100644 --- a/packages/apps/job-launcher/server/src/common/guards/whitelist.auth.ts +++ b/packages/apps/job-launcher/server/src/common/guards/whitelist.auth.ts @@ -1,14 +1,16 @@ import { CanActivate, ExecutionContext, - HttpStatus, Injectable, + Logger, } from '@nestjs/common'; import { WhitelistService } from '../../modules/whitelist/whitelist.service'; -import { ControlledError } from '../errors/controlled'; +import { AuthError } from '../errors'; @Injectable() export class WhitelistAuthGuard implements CanActivate { + private readonly logger = new Logger(WhitelistAuthGuard.name); + constructor(private readonly whitelistService: WhitelistService) {} async canActivate(context: ExecutionContext): Promise { @@ -16,14 +18,15 @@ export class WhitelistAuthGuard implements CanActivate { const user = request.user; if (!user) { - throw new ControlledError('User not found.', HttpStatus.UNAUTHORIZED); + this.logger.error('User object is missing in the request.', request); + throw new AuthError('User not found.'); } const isWhitelisted = await this.whitelistService.isUserWhitelisted( user.id, ); if (!isWhitelisted) { - throw new ControlledError('Unauthorized.', HttpStatus.UNAUTHORIZED); + throw new AuthError('Unauthorized.'); } return true; diff --git a/packages/apps/job-launcher/server/src/common/pipes/validation.ts b/packages/apps/job-launcher/server/src/common/pipes/validation.ts index eec0dea408..9654e9fd60 100644 --- a/packages/apps/job-launcher/server/src/common/pipes/validation.ts +++ b/packages/apps/job-launcher/server/src/common/pipes/validation.ts @@ -1,27 +1,18 @@ import { - HttpStatus, Injectable, - ValidationError, + ValidationError as ValidError, ValidationPipe, ValidationPipeOptions, } from '@nestjs/common'; -import { ControlledError } from '../errors/controlled'; +import { ValidationError } from '../errors'; @Injectable() export class HttpValidationPipe extends ValidationPipe { constructor(options?: ValidationPipeOptions) { super({ - exceptionFactory: (errors: ValidationError[]): ControlledError => { - const errorMessages = errors - .map( - (error) => - Object.values((error as any).constraints) as unknown as string, - ) - .flat(); - throw new ControlledError( - errorMessages.join(', '), - HttpStatus.BAD_REQUEST, - ); + exceptionFactory: (errors: ValidError[]): ValidationError => { + const flattenErrors = this.flattenValidationErrors(errors); + throw new ValidationError(flattenErrors.join(', ')); }, transform: true, whitelist: true, diff --git a/packages/apps/job-launcher/server/src/common/utils/decimal.ts b/packages/apps/job-launcher/server/src/common/utils/decimal.ts index 4bcd4a5a00..a5273f6d6c 100644 --- a/packages/apps/job-launcher/server/src/common/utils/decimal.ts +++ b/packages/apps/job-launcher/server/src/common/utils/decimal.ts @@ -1,6 +1,4 @@ import Decimal from 'decimal.js'; -import { ControlledError } from '../errors/controlled'; -import { HttpStatus } from '@nestjs/common'; export function mul(a: number, b: number): number { const decimalA = new Decimal(a); @@ -16,10 +14,7 @@ export function div(a: number, b: number): number { const decimalB = new Decimal(b); if (decimalB.isZero()) { - throw new ControlledError( - 'Division by zero is not allowed.', - HttpStatus.CONFLICT, - ); + throw new Error('Division by zero is not allowed.'); } const result = decimalA.dividedBy(decimalB); diff --git a/packages/apps/job-launcher/server/src/common/utils/http.ts b/packages/apps/job-launcher/server/src/common/utils/http.ts new file mode 100644 index 0000000000..6c4fb2b45b --- /dev/null +++ b/packages/apps/job-launcher/server/src/common/utils/http.ts @@ -0,0 +1,10 @@ +import { AxiosError } from 'axios'; + +export function formatAxiosError(error: AxiosError) { + return { + name: error.name, + stack: error.stack, + cause: error.cause, + message: error.message, + }; +} diff --git a/packages/apps/job-launcher/server/src/common/utils/index.ts b/packages/apps/job-launcher/server/src/common/utils/index.ts index a8cf2bbd7d..e7b7e7410d 100644 --- a/packages/apps/job-launcher/server/src/common/utils/index.ts +++ b/packages/apps/job-launcher/server/src/common/utils/index.ts @@ -1,7 +1,6 @@ -import { HttpStatus } from '@nestjs/common'; import * as crypto from 'crypto'; import { Readable } from 'stream'; -import { ControlledError } from '../errors/controlled'; +import { ValidationError } from '../errors'; export const parseUrl = ( url: string, @@ -76,7 +75,7 @@ export const parseUrl = ( } } - throw new ControlledError('Invalid URL', HttpStatus.BAD_REQUEST); + throw new ValidationError('Invalid URL'); }; export function hashStream(stream: Readable): Promise { diff --git a/packages/apps/job-launcher/server/src/common/utils/signature.spec.ts b/packages/apps/job-launcher/server/src/common/utils/signature.spec.ts index 2b655775f4..ca5ed5620f 100644 --- a/packages/apps/job-launcher/server/src/common/utils/signature.spec.ts +++ b/packages/apps/job-launcher/server/src/common/utils/signature.spec.ts @@ -1,8 +1,7 @@ -import { verifySignature, recoverSigner, signMessage } from './signature'; import { MOCK_ADDRESS, MOCK_PRIVATE_KEY } from '../../../test/constants'; import { ErrorSignature } from '../constants/errors'; -import { ControlledError } from '../errors/controlled'; -import { HttpStatus } from '@nestjs/common'; +import { ConflictError } from '../errors'; +import { recoverSigner, signMessage, verifySignature } from './signature'; jest.doMock('ethers', () => { return { @@ -12,10 +11,7 @@ jest.doMock('ethers', () => { if (message === 'valid-message' && signature === 'valid-signature') { return 'recovered-address'; } else { - throw new ControlledError( - 'Invalid signature', - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError('Invalid signature'); } }); }, @@ -42,12 +38,7 @@ describe('Signature utility', () => { expect(() => { verifySignature(message, invalidSignature, [invalidAddress]); - }).toThrow( - new ControlledError( - ErrorSignature.SignatureNotVerified, - HttpStatus.CONFLICT, - ), - ); + }).toThrow(new ConflictError(ErrorSignature.SignatureNotVerified)); }); it('should throw conflict exception for invalid signature', () => { @@ -56,12 +47,7 @@ describe('Signature utility', () => { expect(() => { verifySignature(message, invalidSignature, [MOCK_ADDRESS]); - }).toThrow( - new ControlledError( - ErrorSignature.InvalidSignature, - HttpStatus.CONFLICT, - ), - ); + }).toThrow(new ConflictError(ErrorSignature.InvalidSignature)); }); }); @@ -81,12 +67,7 @@ describe('Signature utility', () => { expect(() => { recoverSigner(message, invalidSignature); - }).toThrow( - new ControlledError( - ErrorSignature.InvalidSignature, - HttpStatus.CONFLICT, - ), - ); + }).toThrow(new ConflictError(ErrorSignature.InvalidSignature)); }); it('should stringify message object if it is not already a string', async () => { diff --git a/packages/apps/job-launcher/server/src/common/utils/signature.ts b/packages/apps/job-launcher/server/src/common/utils/signature.ts index 34ebeb8eb9..e4dd1ba52b 100644 --- a/packages/apps/job-launcher/server/src/common/utils/signature.ts +++ b/packages/apps/job-launcher/server/src/common/utils/signature.ts @@ -1,7 +1,6 @@ -import { HttpStatus } from '@nestjs/common'; import { ethers } from 'ethers'; import { ErrorSignature } from '../constants/errors'; -import { ControlledError } from '../errors/controlled'; +import { ValidationError } from '../errors'; export function verifySignature( message: object | string, @@ -13,10 +12,7 @@ export function verifySignature( if ( !addresses.some((address) => address.toLowerCase() === signer.toLowerCase()) ) { - throw new ControlledError( - ErrorSignature.SignatureNotVerified, - HttpStatus.CONFLICT, - ); + throw new ValidationError(ErrorSignature.SignatureNotVerified); } return true; @@ -47,9 +43,6 @@ export function recoverSigner( try { return ethers.verifyMessage(message, signature); } catch (_error) { - throw new ControlledError( - ErrorSignature.InvalidSignature, - HttpStatus.CONFLICT, - ); + throw new ValidationError(ErrorSignature.InvalidSignature); } } diff --git a/packages/apps/job-launcher/server/src/common/utils/storage.ts b/packages/apps/job-launcher/server/src/common/utils/storage.ts index d51cb003d5..d9f897ca24 100644 --- a/packages/apps/job-launcher/server/src/common/utils/storage.ts +++ b/packages/apps/job-launcher/server/src/common/utils/storage.ts @@ -1,14 +1,14 @@ import { HttpStatus } from '@nestjs/common'; +import axios from 'axios'; +import { parseString } from 'xml2js'; import { StorageDataDto } from '../../modules/job/job.dto'; -import { AWSRegions, StorageProviders } from '../enums/storage'; import { ErrorBucket } from '../constants/errors'; import { AudinoJobType, CvatJobType, JobRequestType } from '../enums/job'; -import axios from 'axios'; -import { parseString } from 'xml2js'; -import { ControlledError } from '../errors/controlled'; +import { AWSRegions, StorageProviders } from '../enums/storage'; +import { ValidationError } from '../errors'; import { - GCS_HTTP_REGEX_SUBDOMAIN, GCS_HTTP_REGEX_PATH_BASED, + GCS_HTTP_REGEX_SUBDOMAIN, } from './gcstorage'; export function generateBucketUrl( @@ -30,28 +30,19 @@ export function generateBucketUrl( storageData.provider != StorageProviders.GCS && storageData.provider != StorageProviders.LOCAL ) { - throw new ControlledError( - ErrorBucket.InvalidProvider, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorBucket.InvalidProvider); } if (!storageData.bucketName) { - throw new ControlledError(ErrorBucket.EmptyBucket, HttpStatus.BAD_REQUEST); + throw new ValidationError(ErrorBucket.EmptyBucket); } switch (storageData.provider) { case StorageProviders.AWS: if (!storageData.region) { - throw new ControlledError( - ErrorBucket.EmptyRegion, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorBucket.EmptyRegion); } if (!isRegion(storageData.region)) { - throw new ControlledError( - ErrorBucket.InvalidRegion, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorBucket.InvalidRegion); } return new URL( `https://${storageData.bucketName}.s3.${ @@ -73,10 +64,7 @@ export function generateBucketUrl( }`, ); default: - throw new ControlledError( - ErrorBucket.InvalidProvider, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorBucket.InvalidProvider); } } diff --git a/packages/apps/job-launcher/server/src/database/base.repository.ts b/packages/apps/job-launcher/server/src/database/base.repository.ts index 32cc350cf4..758525d8b4 100644 --- a/packages/apps/job-launcher/server/src/database/base.repository.ts +++ b/packages/apps/job-launcher/server/src/database/base.repository.ts @@ -6,7 +6,7 @@ import { QueryFailedError, Repository, } from 'typeorm'; -import { handleQueryFailedError } from '../common/errors/database'; +import { handleQueryFailedError } from '../common/errors'; export class BaseRepository extends Repository { private readonly entityManager: EntityManager; diff --git a/packages/apps/job-launcher/server/src/modules/auth/auth.controller.ts b/packages/apps/job-launcher/server/src/modules/auth/auth.controller.ts index df1055b929..cdd9440f7f 100644 --- a/packages/apps/job-launcher/server/src/modules/auth/auth.controller.ts +++ b/packages/apps/job-launcher/server/src/modules/auth/auth.controller.ts @@ -2,43 +2,42 @@ import { Body, ClassSerializerInterceptor, Controller, + HttpCode, + Ip, + Logger, Post, Req, + Request, UseGuards, UseInterceptors, - Request, - Logger, - Ip, - HttpCode, - HttpStatus, } from '@nestjs/common'; import { ApiBearerAuth, - ApiTags, - ApiResponse, ApiBody, ApiOperation, + ApiResponse, + ApiTags, } from '@nestjs/swagger'; +import { ErrorAuth } from '../../common/constants/errors'; import { Public } from '../../common/decorators'; +import { ValidationError } from '../../common/errors'; +import { JwtAuthGuard } from '../../common/guards'; +import { RequestWithUser } from '../../common/types'; import { UserCreateDto } from '../user/user.dto'; import { ApiKeyDto, AuthDto, ForgotPasswordDto, + RefreshDto, ResendEmailVerificationDto, RestorePasswordDto, SignInDto, VerifyEmailDto, - RefreshDto, } from './auth.dto'; import { AuthService } from './auth.service'; -import { JwtAuthGuard } from '../../common/guards'; -import { RequestWithUser } from '../../common/types'; -import { ErrorAuth } from '../../common/constants/errors'; -import { TokenRepository } from './token.repository'; import { TokenType } from './token.entity'; -import { ControlledError } from '../../common/errors/controlled'; +import { TokenRepository } from './token.repository'; @ApiTags('Auth') @ApiResponse({ @@ -251,10 +250,7 @@ export class AuthJwtController { e.message, `${AuthJwtController.name} - ${ErrorAuth.ApiKeyCouldNotBeCreatedOrUpdated}`, ); - throw new ControlledError( - ErrorAuth.ApiKeyCouldNotBeCreatedOrUpdated, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorAuth.ApiKeyCouldNotBeCreatedOrUpdated); } } } diff --git a/packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts b/packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts index 0a25d4d542..a525dffe4b 100644 --- a/packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/auth/auth.service.spec.ts @@ -1,14 +1,18 @@ -import { Test } from '@nestjs/testing'; -import { AuthService } from './auth.service'; -import { TokenRepository } from './token.repository'; -import { ConfigService } from '@nestjs/config'; -import { HttpService } from '@nestjs/axios'; +jest.mock('@human-protocol/sdk'); +jest.mock('../../common/utils/hcaptcha', () => ({ + verifyToken: jest.fn().mockReturnValue({ success: true }), +})); + +jest.mock('uuid', () => ({ + v4: jest.fn().mockReturnValue('mocked-uuid'), +})); + import { createMock } from '@golevelup/ts-jest'; -import { UserRepository } from '../user/user.repository'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; -import { UserService } from '../user/user.service'; -import { UserEntity } from '../user/user.entity'; -import { ErrorAuth, ErrorUser } from '../../common/constants/errors'; +import { Test } from '@nestjs/testing'; +import { v4 } from 'uuid'; import { MOCK_ACCESS_TOKEN, MOCK_EMAIL, @@ -19,27 +23,26 @@ import { MOCK_REFRESH_TOKEN, mockConfig, } from '../../../test/constants'; -import { TokenEntity, TokenType } from './token.entity'; -import { v4 } from 'uuid'; -import { PaymentService } from '../payment/payment.service'; +import { AuthConfigService } from '../../common/config/auth-config.service'; +import { ServerConfigService } from '../../common/config/server-config.service'; +import { SENDGRID_TEMPLATES, SERVICE_NAME } from '../../common/constants'; +import { ErrorAuth, ErrorUser } from '../../common/constants/errors'; import { UserStatus } from '../../common/enums/user'; +import { + ConflictError, + ForbiddenError, + NotFoundError, +} from '../../common/errors'; +import { PaymentService } from '../payment/payment.service'; import { SendGridService } from '../sendgrid/sendgrid.service'; -import { SENDGRID_TEMPLATES, SERVICE_NAME } from '../../common/constants'; -import { ApiKeyRepository } from './apikey.repository'; -import { ServerConfigService } from '../../common/config/server-config.service'; -import { AuthConfigService } from '../../common/config/auth-config.service'; -import { ControlledError } from '../../common/errors/controlled'; -import { HttpStatus } from '@nestjs/common'; +import { UserEntity } from '../user/user.entity'; +import { UserRepository } from '../user/user.repository'; +import { UserService } from '../user/user.service'; import { WhitelistService } from '../whitelist/whitelist.service'; - -jest.mock('@human-protocol/sdk'); -jest.mock('../../common/utils/hcaptcha', () => ({ - verifyToken: jest.fn().mockReturnValue({ success: true }), -})); - -jest.mock('uuid', () => ({ - v4: jest.fn().mockReturnValue('mocked-uuid'), -})); +import { ApiKeyRepository } from './apikey.repository'; +import { AuthService } from './auth.service'; +import { TokenEntity, TokenType } from './token.entity'; +import { TokenRepository } from './token.repository'; describe('AuthService', () => { let authService: AuthService; @@ -142,14 +145,11 @@ describe('AuthService', () => { }); }); - it('should throw UnauthorizedException if user credentials are invalid', async () => { + it('should throw ForbiddenError if user credentials are invalid', async () => { getByCredentialsMock.mockResolvedValue(undefined); await expect(authService.signin(signInDto)).rejects.toThrow( - new ControlledError( - ErrorAuth.InvalidEmailOrPassword, - HttpStatus.FORBIDDEN, - ), + new ForbiddenError(ErrorAuth.InvalidEmailOrPassword), ); expect(userService.getByCredentials).toHaveBeenCalledWith( @@ -212,7 +212,7 @@ describe('AuthService', () => { .mockResolvedValue(userEntity as any); await expect(authService.signup(userCreateDto)).rejects.toThrow( - new ControlledError(ErrorUser.DuplicatedEmail, HttpStatus.BAD_REQUEST), + new ConflictError(ErrorUser.DuplicatedEmail), ); expect(userRepository.findByEmail).toHaveBeenCalledWith(userEntity.email); @@ -327,23 +327,19 @@ describe('AuthService', () => { jest.clearAllMocks(); }); - it('should throw NotFound exception if user is not found', () => { + it('should throw NotFoundError if user is not found', () => { findByEmailMock.mockResolvedValue(null); expect( authService.forgotPassword({ email: 'user@example.com' }), - ).rejects.toThrow( - new ControlledError(ErrorUser.NotFound, HttpStatus.NO_CONTENT), - ); + ).rejects.toThrow(new NotFoundError(ErrorUser.NotFound)); }); - it('should throw Unauthorized exception if user is not active', () => { + it('should throw ForbiddenError if user is not active', () => { userEntity.status = UserStatus.INACTIVE; findByEmailMock.mockResolvedValue(userEntity); expect( authService.forgotPassword({ email: 'user@example.com' }), - ).rejects.toThrow( - new ControlledError(ErrorUser.UserNotActive, HttpStatus.FORBIDDEN), - ); + ).rejects.toThrow(new ForbiddenError(ErrorUser.UserNotActive)); }); it('should remove existing token if it exists', async () => { @@ -410,9 +406,7 @@ describe('AuthService', () => { password: 'password', hCaptchaToken: 'token', }), - ).rejects.toThrow( - new ControlledError(ErrorAuth.InvalidToken, HttpStatus.FORBIDDEN), - ); + ).rejects.toThrow(new ForbiddenError(ErrorAuth.InvalidToken)); }); it('should throw an error if token is expired', () => { @@ -425,9 +419,7 @@ describe('AuthService', () => { password: 'password', hCaptchaToken: 'token', }), - ).rejects.toThrow( - new ControlledError(ErrorAuth.TokenExpired, HttpStatus.FORBIDDEN), - ); + ).rejects.toThrow(new ForbiddenError(ErrorAuth.TokenExpired)); }); it('should update password and send email', async () => { @@ -478,14 +470,14 @@ describe('AuthService', () => { it('should throw an error if token is not found', () => { findTokenMock.mockResolvedValue(null); expect(authService.emailVerification({ token: 'token' })).rejects.toThrow( - new ControlledError(ErrorAuth.NotFound, HttpStatus.FORBIDDEN), + new NotFoundError(ErrorAuth.NotFound), ); }); it('should throw an error if token is expired', () => { tokenEntity.expiresAt = new Date(new Date().getDate() - 1); findTokenMock.mockResolvedValue(tokenEntity as TokenEntity); expect(authService.emailVerification({ token: 'token' })).rejects.toThrow( - new ControlledError(ErrorAuth.TokenExpired, HttpStatus.FORBIDDEN), + new ForbiddenError(ErrorAuth.TokenExpired), ); }); @@ -528,9 +520,7 @@ describe('AuthService', () => { findByEmailMock.mockResolvedValue(null); expect( authService.resendEmailVerification({ email: 'user@example.com' }), - ).rejects.toThrow( - new ControlledError(ErrorUser.NotFound, HttpStatus.NO_CONTENT), - ); + ).rejects.toThrow(new NotFoundError(ErrorUser.NotFound)); }); it('should throw an error if user is not pending', () => { @@ -538,9 +528,7 @@ describe('AuthService', () => { findByEmailMock.mockResolvedValue(userEntity); expect( authService.resendEmailVerification({ email: 'user@example.com' }), - ).rejects.toThrow( - new ControlledError(ErrorUser.NotFound, HttpStatus.NO_CONTENT), - ); + ).rejects.toThrow(new ConflictError(ErrorUser.InvalidStatus)); }); it('should create token and send email', async () => { diff --git a/packages/apps/job-launcher/server/src/modules/auth/auth.service.ts b/packages/apps/job-launcher/server/src/modules/auth/auth.service.ts index 534cd4afc2..278f12c0a2 100644 --- a/packages/apps/job-launcher/server/src/modules/auth/auth.service.ts +++ b/packages/apps/job-launcher/server/src/modules/auth/auth.service.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { HttpStatus, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { ErrorAuth, ErrorUser } from '../../common/constants/errors'; @@ -22,16 +22,20 @@ import { TokenRepository } from './token.repository'; import { AuthConfigService } from '../../common/config/auth-config.service'; import { ServerConfigService } from '../../common/config/server-config.service'; -import { SendGridService } from '../sendgrid/sendgrid.service'; +import * as crypto from 'crypto'; +import { + ConflictError, + ForbiddenError, + NotFoundError, +} from '../../common/errors'; import { SENDGRID_TEMPLATES, SERVICE_NAME } from '../../common/constants'; import { generateHash } from '../../common/utils/crypto'; -import { ApiKeyRepository } from './apikey.repository'; -import * as crypto from 'crypto'; import { verifyToken } from '../../common/utils/hcaptcha'; +import { SendGridService } from '../sendgrid/sendgrid.service'; import { UserRepository } from '../user/user.repository'; -import { ApiKeyEntity } from './apikey.entity'; -import { ControlledError } from '../../common/errors/controlled'; import { WhitelistService } from '../whitelist/whitelist.service'; +import { ApiKeyEntity } from './apikey.entity'; +import { ApiKeyRepository } from './apikey.repository'; @Injectable() export class AuthService { @@ -59,9 +63,8 @@ export class AuthService { // ) // ).success // ) { - // throw new ControlledError( + // throw new ForbiddenError( // ErrorAuth.InvalidCaptchaToken, - // HttpStatus.FORBIDDEN, // ); // } const userEntity = await this.userService.getByCredentials( @@ -70,10 +73,7 @@ export class AuthService { ); if (!userEntity) { - throw new ControlledError( - ErrorAuth.InvalidEmailOrPassword, - HttpStatus.FORBIDDEN, - ); + throw new ForbiddenError(ErrorAuth.InvalidEmailOrPassword); } return this.auth(userEntity); @@ -91,17 +91,11 @@ export class AuthService { ) ).success ) { - throw new ControlledError( - ErrorAuth.InvalidCaptchaToken, - HttpStatus.FORBIDDEN, - ); + throw new ForbiddenError(ErrorAuth.InvalidCaptchaToken); } const storedUser = await this.userRepository.findByEmail(data.email); if (storedUser) { - throw new ControlledError( - ErrorUser.DuplicatedEmail, - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError(ErrorUser.DuplicatedEmail); } const userEntity = await this.userService.create(data); @@ -138,11 +132,11 @@ export class AuthService { ); if (!tokenEntity) { - throw new ControlledError(ErrorAuth.InvalidToken, HttpStatus.FORBIDDEN); + throw new ForbiddenError(ErrorAuth.InvalidToken); } if (new Date() > tokenEntity.expiresAt) { - throw new ControlledError(ErrorAuth.TokenExpired, HttpStatus.FORBIDDEN); + throw new ForbiddenError(ErrorAuth.TokenExpired); } return this.auth(tokenEntity.user); @@ -191,11 +185,11 @@ export class AuthService { const userEntity = await this.userRepository.findByEmail(data.email); if (!userEntity) { - throw new ControlledError(ErrorUser.NotFound, HttpStatus.NO_CONTENT); + throw new NotFoundError(ErrorUser.NotFound); } if (userEntity.status !== UserStatus.ACTIVE) { - throw new ControlledError(ErrorUser.UserNotActive, HttpStatus.FORBIDDEN); + throw new ForbiddenError(ErrorUser.UserNotActive); } const existingToken = await this.tokenRepository.findOneByUserIdAndType( @@ -246,10 +240,7 @@ export class AuthService { ) ).success ) { - throw new ControlledError( - ErrorAuth.InvalidCaptchaToken, - HttpStatus.FORBIDDEN, - ); + throw new ForbiddenError(ErrorAuth.InvalidCaptchaToken); } const tokenEntity = await this.tokenRepository.findOneByUuidAndType( @@ -258,11 +249,11 @@ export class AuthService { ); if (!tokenEntity) { - throw new ControlledError(ErrorAuth.InvalidToken, HttpStatus.FORBIDDEN); + throw new ForbiddenError(ErrorAuth.InvalidToken); } if (new Date() > tokenEntity.expiresAt) { - throw new ControlledError(ErrorAuth.TokenExpired, HttpStatus.FORBIDDEN); + throw new ForbiddenError(ErrorAuth.TokenExpired); } await this.userService.updatePassword(tokenEntity.user, data); @@ -288,11 +279,11 @@ export class AuthService { ); if (!tokenEntity) { - throw new ControlledError(ErrorAuth.NotFound, HttpStatus.FORBIDDEN); + throw new NotFoundError(ErrorAuth.NotFound); } if (new Date() > tokenEntity.expiresAt) { - throw new ControlledError(ErrorAuth.TokenExpired, HttpStatus.FORBIDDEN); + throw new ForbiddenError(ErrorAuth.TokenExpired); } tokenEntity.user.status = UserStatus.ACTIVE; @@ -304,8 +295,10 @@ export class AuthService { ): Promise { const userEntity = await this.userRepository.findByEmail(data.email); - if (!userEntity || userEntity?.status != UserStatus.PENDING) { - throw new ControlledError(ErrorUser.NotFound, HttpStatus.NO_CONTENT); + if (!userEntity) { + throw new NotFoundError(ErrorUser.NotFound); + } else if (userEntity?.status != UserStatus.PENDING) { + throw new ConflictError(ErrorUser.InvalidStatus); } const existingToken = await this.tokenRepository.findOneByUserIdAndType( @@ -378,7 +371,7 @@ export class AuthService { const apiKeyEntity = await this.apiKeyRepository.findAPIKeyByUserId(userId); if (!apiKeyEntity) { - throw new ControlledError(ErrorAuth.ApiKeyNotFound, HttpStatus.FORBIDDEN); + throw new ForbiddenError(ErrorAuth.ApiKeyNotFound); } const hash = await generateHash( @@ -398,7 +391,7 @@ export class AuthService { const apiKeyEntity = await this.apiKeyRepository.findAPIKeyById(apiKeyId); if (!apiKeyEntity) { - throw new ControlledError(ErrorAuth.ApiKeyNotFound, HttpStatus.FORBIDDEN); + throw new ForbiddenError(ErrorAuth.ApiKeyNotFound); } const hash = await generateHash( apiKey, diff --git a/packages/apps/job-launcher/server/src/modules/auth/strategy/jwt.http.ts b/packages/apps/job-launcher/server/src/modules/auth/strategy/jwt.http.ts index bdf08ceb28..112a63ba2d 100644 --- a/packages/apps/job-launcher/server/src/modules/auth/strategy/jwt.http.ts +++ b/packages/apps/job-launcher/server/src/modules/auth/strategy/jwt.http.ts @@ -1,18 +1,18 @@ -import { ExtractJwt, Strategy } from 'passport-jwt'; +import { Injectable, Req } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; -import { HttpStatus, Injectable, Req } from '@nestjs/common'; +import { ExtractJwt, Strategy } from 'passport-jwt'; -import { UserEntity } from '../../user/user.entity'; +import { AuthConfigService } from '../../../common/config/auth-config.service'; import { LOGOUT_PATH, RESEND_EMAIL_VERIFICATION_PATH, } from '../../../common/constants'; import { UserStatus } from '../../../common/enums/user'; -import { AuthConfigService } from '../../../common/config/auth-config.service'; +import { AuthError } from '../../../common/errors'; +import { UserEntity } from '../../user/user.entity'; import { UserRepository } from '../../user/user.repository'; -import { ControlledError } from '../../../common/errors/controlled'; -import { TokenRepository } from '../token.repository'; import { TokenType } from '../token.entity'; +import { TokenRepository } from '../token.repository'; @Injectable() export class JwtHttpStrategy extends PassportStrategy(Strategy, 'jwt-http') { @@ -36,7 +36,7 @@ export class JwtHttpStrategy extends PassportStrategy(Strategy, 'jwt-http') { const user = await this.userRepository.findById(payload.userId); if (!user) { - throw new ControlledError('User not found', HttpStatus.UNAUTHORIZED); + throw new AuthError('User not found'); } if ( @@ -44,7 +44,7 @@ export class JwtHttpStrategy extends PassportStrategy(Strategy, 'jwt-http') { request.url !== RESEND_EMAIL_VERIFICATION_PATH && request.url !== LOGOUT_PATH ) { - throw new ControlledError('User not active', HttpStatus.UNAUTHORIZED); + throw new AuthError('User not active'); } const token = await this.tokenRepository.findOneByUserIdAndType( @@ -53,10 +53,7 @@ export class JwtHttpStrategy extends PassportStrategy(Strategy, 'jwt-http') { ); if (!token) { - throw new ControlledError( - 'User is not authorized', - HttpStatus.UNAUTHORIZED, - ); + throw new AuthError('User is not authorized'); } return user; diff --git a/packages/apps/job-launcher/server/src/modules/content-moderation/content-moderation-request.repository.ts b/packages/apps/job-launcher/server/src/modules/content-moderation/content-moderation-request.repository.ts index 009814295d..179b6c1092 100644 --- a/packages/apps/job-launcher/server/src/modules/content-moderation/content-moderation-request.repository.ts +++ b/packages/apps/job-launcher/server/src/modules/content-moderation/content-moderation-request.repository.ts @@ -6,7 +6,7 @@ import { ContentModerationRequestStatus } from '../../common/enums/content-moder import { BaseRepository } from '../../database/base.repository'; import { ContentModerationRequestEntity } from './content-moderation-request.entity'; import { QueryFailedError } from 'typeorm'; -import { handleQueryFailedError } from '../../common/errors/database'; +import { handleQueryFailedError } from '../../common/errors'; @Injectable() export class ContentModerationRequestRepository extends BaseRepository { diff --git a/packages/apps/job-launcher/server/src/modules/content-moderation/gcv-content-moderation.service.spec.ts b/packages/apps/job-launcher/server/src/modules/content-moderation/gcv-content-moderation.service.spec.ts index ba784488da..84bce6d2ef 100644 --- a/packages/apps/job-launcher/server/src/modules/content-moderation/gcv-content-moderation.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/content-moderation/gcv-content-moderation.service.spec.ts @@ -1,3 +1,13 @@ +jest.mock('@google-cloud/storage'); +jest.mock('@google-cloud/vision'); +jest.mock('../../common/utils/slack', () => ({ + sendSlackNotification: jest.fn(), +})); +jest.mock('../../common/utils/storage', () => ({ + ...jest.requireActual('../../common/utils/storage'), + listObjectsInBucket: jest.fn(), +})); + import { faker } from '@faker-js/faker'; import { Storage } from '@google-cloud/storage'; import { ImageAnnotatorClient } from '@google-cloud/vision'; @@ -6,28 +16,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SlackConfigService } from '../../common/config/slack-config.service'; import { VisionConfigService } from '../../common/config/vision-config.service'; import { ErrorContentModeration } from '../../common/constants/errors'; -import { ContentModerationLevel } from '../../common/enums/gcv'; import { ContentModerationRequestStatus } from '../../common/enums/content-moderation'; +import { ContentModerationLevel } from '../../common/enums/gcv'; import { JobStatus } from '../../common/enums/job'; -import { ControlledError } from '../../common/errors/controlled'; +import { sendSlackNotification } from '../../common/utils/slack'; +import { listObjectsInBucket } from '../../common/utils/storage'; import { JobEntity } from '../job/job.entity'; import { JobRepository } from '../job/job.repository'; +import { ManifestService } from '../manifest/manifest.service'; import { ContentModerationRequestEntity } from './content-moderation-request.entity'; import { ContentModerationRequestRepository } from './content-moderation-request.repository'; import { GCVContentModerationService } from './gcv-content-moderation.service'; -import { sendSlackNotification } from '../../common/utils/slack'; -import { listObjectsInBucket } from '../../common/utils/storage'; -import { ManifestService } from '../manifest/manifest.service'; - -jest.mock('@google-cloud/storage'); -jest.mock('@google-cloud/vision'); -jest.mock('../../common/utils/slack', () => ({ - sendSlackNotification: jest.fn(), -})); -jest.mock('../../common/utils/storage', () => ({ - ...jest.requireActual('../../common/utils/storage'), - listObjectsInBucket: jest.fn(), -})); describe('GCVContentModerationService', () => { let service: GCVContentModerationService; @@ -465,14 +464,14 @@ describe('GCVContentModerationService', () => { ); }); - it('should throw ControlledError if vision call fails', async () => { + it('should throw Error if vision call fails', async () => { mockVisionClient.asyncBatchAnnotateImages.mockRejectedValueOnce( new Error('Vision failure'), ); await expect( (service as any).asyncBatchAnnotateImages([], 'my-file'), - ).rejects.toThrow(ControlledError); + ).rejects.toThrow(Error); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/content-moderation/gcv-content-moderation.service.ts b/packages/apps/job-launcher/server/src/modules/content-moderation/gcv-content-moderation.service.ts index 73b7fbff23..d4bd6f8b0c 100644 --- a/packages/apps/job-launcher/server/src/modules/content-moderation/gcv-content-moderation.service.ts +++ b/packages/apps/job-launcher/server/src/modules/content-moderation/gcv-content-moderation.service.ts @@ -1,6 +1,7 @@ import { Storage } from '@google-cloud/storage'; import { ImageAnnotatorClient, protos } from '@google-cloud/vision'; -import { HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; +import NodeCache from 'node-cache'; import { SlackConfigService } from '../../common/config/slack-config.service'; import { VisionConfigService } from '../../common/config/vision-config.service'; import { @@ -8,13 +9,12 @@ import { GCV_CONTENT_MODERATION_BATCH_SIZE_PER_TASK, } from '../../common/constants'; import { ErrorContentModeration } from '../../common/constants/errors'; +import { ContentModerationRequestStatus } from '../../common/enums/content-moderation'; import { ContentModerationFeature, ContentModerationLevel, } from '../../common/enums/gcv'; -import { ContentModerationRequestStatus } from '../../common/enums/content-moderation'; import { JobStatus } from '../../common/enums/job'; -import { ControlledError } from '../../common/errors/controlled'; import { constructGcsPath, convertToGCSPath, @@ -25,13 +25,12 @@ import { sendSlackNotification } from '../../common/utils/slack'; import { listObjectsInBucket } from '../../common/utils/storage'; import { JobEntity } from '../job/job.entity'; import { JobRepository } from '../job/job.repository'; +import { CvatManifestDto } from '../manifest/manifest.dto'; +import { ManifestService } from '../manifest/manifest.service'; import { ContentModerationRequestEntity } from './content-moderation-request.entity'; import { ContentModerationRequestRepository } from './content-moderation-request.repository'; -import { IContentModeratorService } from './content-moderation.interface'; import { ModerationResultDto } from './content-moderation.dto'; -import NodeCache from 'node-cache'; -import { ManifestService } from '../manifest/manifest.service'; -import { CvatManifestDto } from '../manifest/manifest.dto'; +import { IContentModeratorService } from './content-moderation.interface'; @Injectable() export class GCVContentModerationService implements IContentModeratorService { @@ -333,10 +332,7 @@ export class GCVContentModerationService implements IContentModeratorService { ); } catch (error) { this.logger.error('Error analyzing images:', error); - throw new ControlledError( - ErrorContentModeration.ContentModerationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new Error(ErrorContentModeration.ContentModerationFailed); } } @@ -379,10 +375,7 @@ export class GCVContentModerationService implements IContentModeratorService { const [files] = await bucket.getFiles({ prefix: bucketPrefix }); if (!files || files.length === 0) { - throw new ControlledError( - ErrorContentModeration.NoResultsFound, - HttpStatus.NOT_FOUND, - ); + throw new Error(ErrorContentModeration.NoResultsFound); } const allResponses = []; @@ -397,15 +390,11 @@ export class GCVContentModerationService implements IContentModeratorService { } return this.categorizeModerationResults(allResponses); } catch (err) { - this.logger.error('Error collecting moderation results:', err); - if (err instanceof ControlledError) { + if (err.message === ErrorContentModeration.NoResultsFound) { throw err; - } else { - throw new ControlledError( - ErrorContentModeration.ResultsParsingFailed, - HttpStatus.INTERNAL_SERVER_ERROR, - ); } + this.logger.error('Error collecting moderation results:', err); + throw new Error(ErrorContentModeration.ResultsParsingFailed); } } diff --git a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts index 3f04992874..dd397c871f 100644 --- a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.spec.ts @@ -1,6 +1,19 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { CronJobType } from '../../common/enums/cron-job'; +jest.mock('@human-protocol/sdk', () => ({ + ...jest.requireActual('@human-protocol/sdk'), + EscrowClient: { + build: jest.fn().mockImplementation(() => ({ + createEscrow: jest.fn().mockResolvedValue(MOCK_ADDRESS), + setup: jest.fn().mockResolvedValue(null), + fund: jest.fn().mockResolvedValue(null), + })), + }, + KVStoreUtils: { + get: jest.fn(), + }, + EscrowUtils: { + getStatusEvents: jest.fn(), + }, +})); import { faker } from '@faker-js/faker'; import { createMock } from '@golevelup/ts-jest'; @@ -15,8 +28,8 @@ import { } from '@human-protocol/sdk'; import { StatusEvent } from '@human-protocol/sdk/dist/graphql'; import { HttpService } from '@nestjs/axios'; -import { HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; import { ethers } from 'ethers'; import { DeepPartial } from 'typeorm'; import { @@ -37,9 +50,10 @@ import { ErrorContentModeration, ErrorCronJob, } from '../../common/constants/errors'; +import { CronJobType } from '../../common/enums/cron-job'; import { CvatJobType, FortuneJobType, JobStatus } from '../../common/enums/job'; import { WebhookStatus } from '../../common/enums/webhook'; -import { ControlledError } from '../../common/errors/controlled'; +import { ConflictError } from '../../common/errors'; import { ContentModerationRequestRepository } from '../content-moderation/content-moderation-request.repository'; import { GCVContentModerationService } from '../content-moderation/gcv-content-moderation.service'; import { JobEntity } from '../job/job.entity'; @@ -62,23 +76,6 @@ import { CronJobEntity } from './cron-job.entity'; import { CronJobRepository } from './cron-job.repository'; import { CronJobService } from './cron-job.service'; -jest.mock('@human-protocol/sdk', () => ({ - ...jest.requireActual('@human-protocol/sdk'), - EscrowClient: { - build: jest.fn().mockImplementation(() => ({ - createEscrow: jest.fn().mockResolvedValue(MOCK_ADDRESS), - setup: jest.fn().mockResolvedValue(null), - fund: jest.fn().mockResolvedValue(null), - })), - }, - KVStoreUtils: { - get: jest.fn(), - }, - EscrowUtils: { - getStatusEvents: jest.fn(), - }, -})); - describe('CronJobService', () => { let service: CronJobService, repository: CronJobRepository, @@ -327,7 +324,7 @@ describe('CronJobService', () => { .mockResolvedValue(cronJobEntity); await expect(service.completeCronJob(cronJobEntity)).rejects.toThrow( - new ControlledError(ErrorCronJob.Completed, HttpStatus.BAD_REQUEST), + new ConflictError(ErrorCronJob.Completed), ); expect(updateOneSpy).not.toHaveBeenCalled(); }); diff --git a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts index 2e972b05a2..482d5bc904 100644 --- a/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/cron-job/cron-job.service.ts @@ -1,4 +1,4 @@ -import { HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { v4 as uuidv4 } from 'uuid'; import { ErrorContentModeration, @@ -18,7 +18,7 @@ import { OracleType, WebhookStatus, } from '../../common/enums/webhook'; -import { ControlledError } from '../../common/errors/controlled'; +import { ConflictError, NotFoundError } from '../../common/errors'; import { GCVContentModerationService } from '../content-moderation/gcv-content-moderation.service'; import { JobEntity } from '../job/job.entity'; import { JobRepository } from '../job/job.repository'; @@ -75,7 +75,7 @@ export class CronJobService { cronJobEntity: CronJobEntity, ): Promise { if (cronJobEntity.completedAt) { - throw new ControlledError(ErrorCronJob.Completed, HttpStatus.BAD_REQUEST); + throw new ConflictError(ErrorCronJob.Completed); } cronJobEntity.completedAt = new Date(); @@ -395,10 +395,7 @@ export class CronJobService { ); if (!jobEntity) { this.logger.log(ErrorJob.NotFound, JobService.name); - throw new ControlledError( - ErrorJob.NotFound, - HttpStatus.BAD_REQUEST, - ); + throw new NotFoundError(ErrorJob.NotFound); } if ( jobEntity.escrowAddress && diff --git a/packages/apps/job-launcher/server/src/modules/job/job.controller.ts b/packages/apps/job-launcher/server/src/modules/job/job.controller.ts index fb934f4cd5..cd2b584083 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.controller.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.controller.ts @@ -1,8 +1,8 @@ +import { ChainId } from '@human-protocol/sdk'; import { Body, Controller, Get, - HttpStatus, Param, Patch, Post, @@ -13,36 +13,35 @@ import { } from '@nestjs/common'; import { ApiBearerAuth, - ApiOperation, - ApiTags, ApiBody, + ApiOperation, ApiResponse, + ApiTags, } from '@nestjs/swagger'; +import { Web3ConfigService } from '../../common/config/web3-config.service'; +import { MUTEX_TIMEOUT } from '../../common/constants'; +import { ApiKey } from '../../common/decorators'; +import { FortuneJobType } from '../../common/enums/job'; +import { Web3Env } from '../../common/enums/web3'; +import { ForbiddenError } from '../../common/errors'; import { JwtAuthGuard } from '../../common/guards'; +import { PageDto } from '../../common/pagination/pagination.dto'; import { RequestWithUser } from '../../common/types'; +import { MutexManagerService } from '../mutex/mutex-manager.service'; import { - JobFortuneDto, + FortuneFinalResultDto, + GetJobsDto, + JobAudinoDto, + JobCancelDto, JobCvatDto, - JobListDto, JobDetailsDto, + JobFortuneDto, JobIdDto, - FortuneFinalResultDto, + JobListDto, // JobCaptchaDto, JobQuickLaunchDto, - JobCancelDto, - GetJobsDto, - JobAudinoDto, } from './job.dto'; import { JobService } from './job.service'; -import { FortuneJobType } from '../../common/enums/job'; -import { ApiKey } from '../../common/decorators'; -import { ChainId } from '@human-protocol/sdk'; -import { ControlledError } from '../../common/errors/controlled'; -import { PageDto } from '../../common/pagination/pagination.dto'; -import { MutexManagerService } from '../mutex/mutex-manager.service'; -import { MUTEX_TIMEOUT } from '../../common/constants'; -import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { Web3Env } from '../../common/enums/web3'; @ApiBearerAuth() @UseGuards(JwtAuthGuard) @@ -125,7 +124,7 @@ export class JobController { @Request() req: RequestWithUser, ): Promise { if (this.web3ConfigService.env === Web3Env.MAINNET) { - throw new ControlledError('Disabled', HttpStatus.METHOD_NOT_ALLOWED); + throw new ForbiddenError('Disabled'); } return await this.mutexManagerService.runExclusive( @@ -205,7 +204,7 @@ export class JobController { @Request() req: RequestWithUser, ): Promise { if (this.web3ConfigService.env === Web3Env.MAINNET) { - throw new ControlledError('Disabled', HttpStatus.METHOD_NOT_ALLOWED); + throw new ForbiddenError('Disabled'); } return await this.mutexManagerService.runExclusive( @@ -244,9 +243,8 @@ export class JobController { // @Body() data: JobCaptchaDto, // @Request() req: RequestWithUser, // ): Promise { - // throw new ControlledError( + // throw new ForbiddenError( // 'Hcaptcha jobs disabled temporally', - // HttpStatus.UNAUTHORIZED, // ); // return await this.mutexManagerService.runExclusive( // { id: `user${req.user.id}` }, diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 08ba57620e..8b3085f130 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -9,15 +9,12 @@ import { NETWORKS, StorageParams, } from '@human-protocol/sdk'; -import { - HttpStatus, - Inject, - Injectable, - Logger, - ValidationError, -} from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; -import { validate } from 'class-validator'; +import { + ValidationError as ClassValidationError, + validate, +} from 'class-validator'; import { ethers } from 'ethers'; import { ServerConfigService } from '../../common/config/server-config.service'; import { CANCEL_JOB_STATUSES } from '../../common/constants'; @@ -39,7 +36,12 @@ import { } from '../../common/enums/job'; import { FiatCurrency } from '../../common/enums/payment'; import { EventType, OracleType } from '../../common/enums/webhook'; -import { ControlledError } from '../../common/errors/controlled'; +import { + ConflictError, + NotFoundError, + ServerError, + ValidationError, +} from '../../common/errors'; import { PageDto } from '../../common/pagination/pagination.dto'; import { parseUrl } from '../../common/utils'; import { add, div, max, mul } from '../../common/utils/decimal'; @@ -148,10 +150,7 @@ export class JobService { user.stripeCustomerId, )) ) - throw new ControlledError( - ErrorJob.NotActiveCard, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.NotActiveCard); } const feePercentage = Number( @@ -237,10 +236,7 @@ export class JobService { dto.qualifications.forEach((qualification) => { if (!validQualificationReferences.includes(qualification)) { - throw new ControlledError( - ErrorQualification.InvalidQualification, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorQualification.InvalidQualification); } }); } @@ -252,10 +248,7 @@ export class JobService { const { filename } = parseUrl(dto.manifestUrl); if (!filename) { - throw new ControlledError( - ErrorJob.ManifestHashNotExist, - HttpStatus.CONFLICT, - ); + throw new ValidationError(ErrorJob.ManifestHashNotExist); } jobEntity.manifestHash = filename; @@ -341,7 +334,7 @@ export class JobService { ); if (!escrowAddress) { - throw new ControlledError(ErrorEscrow.NotCreated, HttpStatus.NOT_FOUND); + throw new ConflictError(ErrorEscrow.NotCreated); } jobEntity.status = JobStatus.CREATED; @@ -430,7 +423,7 @@ export class JobService { ); if (!jobEntity) { - throw new ControlledError(ErrorJob.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.NotFound); } await this.requestToCancelJob(jobEntity); @@ -449,7 +442,7 @@ export class JobService { ); if (!jobEntity || (jobEntity && jobEntity.userId !== userId)) { - throw new ControlledError(ErrorJob.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.NotFound); } await this.requestToCancelJob(jobEntity); @@ -467,10 +460,7 @@ export class JobService { private async requestToCancelJob(jobEntity: JobEntity): Promise { if (!CANCEL_JOB_STATUSES.includes(jobEntity.status)) { - throw new ControlledError( - ErrorJob.InvalidStatusCancellation, - HttpStatus.CONFLICT, - ); + throw new ConflictError(ErrorJob.InvalidStatusCancellation); } let status = JobStatus.CANCELED; @@ -496,10 +486,7 @@ export class JobService { } if (status === JobStatus.FAILED) { - throw new ControlledError( - ErrorJob.CancelWhileProcessing, - HttpStatus.CONFLICT, - ); + throw new ConflictError(ErrorJob.CancelWhileProcessing); } if (status === JobStatus.CANCELED) { @@ -545,11 +532,7 @@ export class JobService { return new PageDto(data.page!, data.pageSize!, itemCount, jobs); } catch (error) { - throw new ControlledError( - error.message, - HttpStatus.BAD_REQUEST, - error.stack, - ); + throw new ServerError(error.message, error.stack); } } @@ -563,11 +546,11 @@ export class JobService { ); if (!jobEntity) { - throw new ControlledError(ErrorJob.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.NotFound); } if (!jobEntity.escrowAddress) { - throw new ControlledError(ErrorJob.ResultNotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.ResultNotFound); } const signer = this.web3Service.getSigner(jobEntity.chainId); @@ -578,7 +561,7 @@ export class JobService { ); if (!finalResultUrl) { - throw new ControlledError(ErrorJob.ResultNotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.ResultNotFound); } if (jobEntity.requestType === FortuneJobType.FORTUNE) { @@ -587,18 +570,15 @@ export class JobService { )) as Array; if (!data.length) { - throw new ControlledError( - ErrorJob.ResultNotFound, - HttpStatus.NOT_FOUND, - ); + throw new NotFoundError(ErrorJob.ResultNotFound); } - const allFortuneValidationErrors: ValidationError[] = []; + const allFortuneValidationErrors: ClassValidationError[] = []; for (const fortune of data) { const fortuneDtoCheck = new FortuneFinalResultDto(); Object.assign(fortuneDtoCheck, fortune); - const fortuneValidationErrors: ValidationError[] = + const fortuneValidationErrors: ClassValidationError[] = await validate(fortuneDtoCheck); allFortuneValidationErrors.push(...fortuneValidationErrors); } @@ -609,10 +589,7 @@ export class JobService { JobService.name, allFortuneValidationErrors, ); - throw new ControlledError( - ErrorJob.ResultValidationFailed, - HttpStatus.NOT_FOUND, - ); + throw new ValidationError(ErrorJob.ResultValidationFailed); } return data; } @@ -632,18 +609,15 @@ export class JobService { ); if (!jobEntity) { - throw new ControlledError(ErrorJob.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.NotFound); } if (!jobEntity.escrowAddress) { - throw new ControlledError(ErrorJob.ResultNotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.ResultNotFound); } if (jobEntity.requestType === FortuneJobType.FORTUNE) { - throw new ControlledError( - ErrorJob.InvalidRequestType, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.InvalidRequestType); } const signer = this.web3Service.getSigner(jobEntity.chainId); @@ -654,7 +628,7 @@ export class JobService { ); if (!finalResultUrl) { - throw new ControlledError(ErrorJob.ResultNotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.ResultNotFound); } const contents = await this.storageService.downloadFile(finalResultUrl); @@ -705,18 +679,12 @@ export class JobService { escrowStatus === EscrowStatus.Paid || escrowStatus === EscrowStatus.Cancelled ) { - throw new ControlledError( - ErrorEscrow.InvalidStatusCancellation, - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError(ErrorEscrow.InvalidStatusCancellation); } const balance = await escrowClient.getBalance(escrowAddress); if (balance === 0n) { - throw new ControlledError( - ErrorEscrow.InvalidBalanceCancellation, - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError(ErrorEscrow.InvalidBalanceCancellation); } return escrowClient.cancel(escrowAddress, { @@ -726,10 +694,7 @@ export class JobService { public async escrowFailedWebhook(dto: WebhookDataDto): Promise { if (dto.eventType !== EventType.ESCROW_FAILED) { - throw new ControlledError( - ErrorJob.InvalidEventType, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.InvalidEventType); } const jobEntity = await this.jobRepository.findOneByChainIdAndEscrowAddress( dto.chainId, @@ -737,27 +702,21 @@ export class JobService { ); if (!jobEntity) { - throw new ControlledError(ErrorJob.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.NotFound); } if (jobEntity.status !== JobStatus.LAUNCHED) { - throw new ControlledError(ErrorJob.NotLaunched, HttpStatus.CONFLICT); + throw new ConflictError(ErrorJob.NotLaunched); } if (!dto.eventData) { - throw new ControlledError( - 'Event data is required but was not provided.', - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError('Event data is required but was not provided.'); } const reason = dto.eventData.reason; if (!reason) { - throw new ControlledError( - 'Reason is undefined in event data.', - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError('Reason is undefined in event data.'); } jobEntity.status = JobStatus.FAILED; @@ -775,7 +734,7 @@ export class JobService { ); if (!jobEntity) { - throw new ControlledError(ErrorJob.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.NotFound); } const { chainId, escrowAddress, manifestUrl, manifestHash } = jobEntity; @@ -910,7 +869,7 @@ export class JobService { ); if (!jobEntity) { - throw new ControlledError(ErrorJob.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorJob.NotFound); } // If job status already completed by getDetails do nothing @@ -921,10 +880,7 @@ export class JobService { jobEntity.status !== JobStatus.LAUNCHED && jobEntity.status !== JobStatus.PARTIAL ) { - throw new ControlledError( - ErrorJob.InvalidStatusCompletion, - HttpStatus.CONFLICT, - ); + throw new ConflictError(ErrorJob.InvalidStatusCompletion); } jobEntity.status = JobStatus.COMPLETED; diff --git a/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.spec.ts b/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.spec.ts index 2d509f00ad..970468cedf 100644 --- a/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.spec.ts @@ -7,7 +7,6 @@ jest.mock('../../common/utils/storage', () => ({ import { faker } from '@faker-js/faker'; import { createMock } from '@golevelup/ts-jest'; import { Encryption } from '@human-protocol/sdk'; -import { HttpStatus } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { ethers } from 'ethers'; import { AuthConfigService } from '../../common/config/auth-config.service'; @@ -38,7 +37,11 @@ import { JobCaptchaRequestType, JobCaptchaShapeType, } from '../../common/enums/job'; -import { ControlledError } from '../../common/errors/controlled'; +import { + ConflictError, + ServerError, + ValidationError, +} from '../../common/errors'; import { generateBucketUrl, listObjectsInBucket, @@ -278,9 +281,7 @@ describe('ManifestService', () => { tokenFundAmount, tokenFundDecimals, ), - ).rejects.toThrow( - new ControlledError(ErrorJob.DataNotExist, HttpStatus.CONFLICT), - ); + ).rejects.toThrow(new ConflictError(ErrorJob.DataNotExist)); }); it('should throw an error if data does not exist for image skeletons from boxes job type', async () => { @@ -295,9 +296,7 @@ describe('ManifestService', () => { tokenFundAmount, tokenFundDecimals, ), - ).rejects.toThrow( - new ControlledError(ErrorJob.DataNotExist, HttpStatus.CONFLICT), - ); + ).rejects.toThrow(new ConflictError(ErrorJob.DataNotExist)); }); }); @@ -502,7 +501,7 @@ describe('ManifestService', () => { }); }); - it('should throw ControlledError for invalid POLYGON job type without label', async () => { + it('should throw ValidationError for invalid POLYGON job type without label', async () => { const jobDto = createJobCaptchaDto({ annotations: { typeOfJob: JobCaptchaShapeType.POLYGON, @@ -520,10 +519,7 @@ describe('ManifestService', () => { tokenFundDecimals, ), ).rejects.toThrow( - new ControlledError( - ErrorJob.JobParamsValidationFailed, - HttpStatus.BAD_REQUEST, - ), + new ValidationError(ErrorJob.JobParamsValidationFailed), ); }); @@ -577,7 +573,7 @@ describe('ManifestService', () => { }); }); - it('should throw ControlledError for invalid POINT job type without label', async () => { + it('should throw ValidationError for invalid POINT job type without label', async () => { const jobDto = createJobCaptchaDto({ annotations: { typeOfJob: JobCaptchaShapeType.POINT, @@ -595,10 +591,7 @@ describe('ManifestService', () => { tokenFundDecimals, ), ).rejects.toThrow( - new ControlledError( - ErrorJob.JobParamsValidationFailed, - HttpStatus.BAD_REQUEST, - ), + new ValidationError(ErrorJob.JobParamsValidationFailed), ); }); @@ -652,7 +645,7 @@ describe('ManifestService', () => { }); }); - it('should throw ControlledError for invalid BOUNDING_BOX job type without label', async () => { + it('should throw ValidationError for invalid BOUNDING_BOX job type without label', async () => { const jobDto = createJobCaptchaDto({ annotations: { typeOfJob: JobCaptchaShapeType.BOUNDING_BOX, @@ -670,10 +663,7 @@ describe('ManifestService', () => { tokenFundDecimals, ), ).rejects.toThrow( - new ControlledError( - ErrorJob.JobParamsValidationFailed, - HttpStatus.BAD_REQUEST, - ), + new ValidationError(ErrorJob.JobParamsValidationFailed), ); }); @@ -727,7 +717,7 @@ describe('ManifestService', () => { }); }); - it('should throw ControlledError for invalid IMMO job type without label', async () => { + it('should throw ValidationError for invalid IMMO job type without label', async () => { const jobDto = createJobCaptchaDto({ annotations: { typeOfJob: JobCaptchaShapeType.IMMO, @@ -745,14 +735,11 @@ describe('ManifestService', () => { tokenFundDecimals, ), ).rejects.toThrow( - new ControlledError( - ErrorJob.JobParamsValidationFailed, - HttpStatus.BAD_REQUEST, - ), + new ValidationError(ErrorJob.JobParamsValidationFailed), ); }); - it('should throw ControlledError for invalid job type', async () => { + it('should throw ValidationError for invalid job type', async () => { const jobDto = createJobCaptchaDto({ annotations: { typeOfJob: 'INVALID_JOB_TYPE' as JobCaptchaShapeType, @@ -769,12 +756,7 @@ describe('ManifestService', () => { tokenFundAmount, tokenFundDecimals, ), - ).rejects.toThrow( - new ControlledError( - ErrorJob.HCaptchaInvalidJobType, - HttpStatus.CONFLICT, - ), - ); + ).rejects.toThrow(new ValidationError(ErrorJob.HCaptchaInvalidJobType)); }); }); }); @@ -813,7 +795,7 @@ describe('ManifestService', () => { const mockOracleAddresses: string[] = []; mockStorageService.uploadJsonLikeData.mockRejectedValue( - new ControlledError('File not uploaded', HttpStatus.BAD_REQUEST), + new ServerError('File not uploaded'), ); await expect( @@ -822,7 +804,7 @@ describe('ManifestService', () => { mockData, mockOracleAddresses, ), - ).rejects.toThrow(ControlledError); + ).rejects.toThrow(ServerError); }); }); @@ -864,12 +846,7 @@ describe('ManifestService', () => { ); await expect( manifestService.downloadManifest(mockManifestUrl, mockRequestType), - ).rejects.toThrow( - new ControlledError( - ErrorJob.ManifestValidationFailed, - HttpStatus.NOT_FOUND, - ), - ); + ).rejects.toThrow(new ValidationError(ErrorJob.ManifestValidationFailed)); }); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.ts b/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.ts index 0a91fa38d2..3e7e9176d1 100644 --- a/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.ts +++ b/packages/apps/job-launcher/server/src/modules/manifest/manifest.service.ts @@ -6,10 +6,9 @@ import { StorageParams, } from '@human-protocol/sdk'; import { - HttpStatus, + ValidationError as ClassValidationError, Injectable, Logger, - ValidationError, } from '@nestjs/common'; import { validate } from 'class-validator'; import { ethers } from 'ethers'; @@ -44,7 +43,7 @@ import { JobCaptchaShapeType, JobRequestType, } from '../../common/enums/job'; -import { ControlledError } from '../../common/errors/controlled'; +import { ConflictError, ValidationError } from '../../common/errors'; import { generateBucketUrl, listObjectsInBucket, @@ -67,10 +66,10 @@ import { Web3Service } from '../web3/web3.service'; import { AudinoManifestDto, CvatManifestDto, - HCaptchaManifestDto, FortuneManifestDto, - RestrictedAudience, + HCaptchaManifestDto, ManifestDto, + RestrictedAudience, } from './manifest.dto'; @Injectable() @@ -126,10 +125,7 @@ export class ManifestService { ); default: - throw new ControlledError( - ErrorJob.InvalidRequestType, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.InvalidRequestType); } } @@ -144,18 +140,12 @@ export class ManifestService { case CvatJobType.IMAGE_POINTS: const data = await listObjectsInBucket(urls.dataUrl); if (!data || data.length === 0 || !data[0]) - throw new ControlledError( - ErrorJob.DatasetValidationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.DatasetValidationFailed); gt = (await this.storageService.downloadJsonLikeData( `${urls.gtUrl.protocol}//${urls.gtUrl.host}${urls.gtUrl.pathname}`, )) as any; if (!gt || !gt.images || gt.images.length === 0) - throw new ControlledError( - ErrorJob.GroundThuthValidationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.GroundThuthValidationFailed); await this.checkImageConsistency(gt.images, data); @@ -170,10 +160,7 @@ export class ManifestService { )) as any; if (!gt || !gt.images || gt.images.length === 0) { - throw new ControlledError( - ErrorJob.GroundThuthValidationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.GroundThuthValidationFailed); } gtEntries = 0; @@ -203,10 +190,7 @@ export class ManifestService { )) as any; if (!gt || !gt.images || gt.images.length === 0) { - throw new ControlledError( - ErrorJob.GroundThuthValidationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.GroundThuthValidationFailed); } gtEntries = 0; @@ -228,10 +212,7 @@ export class ManifestService { return boxes.annotations.length - gtEntries; default: - throw new ControlledError( - ErrorJob.InvalidRequestType, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.InvalidRequestType); } } @@ -248,10 +229,7 @@ export class ManifestService { ); if (missingFileNames.length !== 0) { - throw new ControlledError( - ErrorJob.ImageConsistency, - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError(ErrorJob.ImageConsistency); } } @@ -298,7 +276,7 @@ export class ManifestService { !dto.data.boxes) || (requestType === CvatJobType.IMAGE_BOXES_FROM_POINTS && !dto.data.points) ) { - throw new ControlledError(ErrorJob.DataNotExist, HttpStatus.CONFLICT); + throw new ConflictError(ErrorJob.DataNotExist); } const urls = { @@ -442,10 +420,7 @@ export class ManifestService { case JobCaptchaShapeType.POLYGON: if (!jobDto.annotations.label) { - throw new ControlledError( - ErrorJob.JobParamsValidationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.JobParamsValidationFailed); } const polygonManifest = { @@ -471,10 +446,7 @@ export class ManifestService { case JobCaptchaShapeType.POINT: if (!jobDto.annotations.label) { - throw new ControlledError( - ErrorJob.JobParamsValidationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.JobParamsValidationFailed); } const pointManifest = { @@ -497,10 +469,7 @@ export class ManifestService { return pointManifest; case JobCaptchaShapeType.BOUNDING_BOX: if (!jobDto.annotations.label) { - throw new ControlledError( - ErrorJob.JobParamsValidationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.JobParamsValidationFailed); } const boundingBoxManifest = { @@ -523,10 +492,7 @@ export class ManifestService { return boundingBoxManifest; case JobCaptchaShapeType.IMMO: if (!jobDto.annotations.label) { - throw new ControlledError( - ErrorJob.JobParamsValidationFailed, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorJob.JobParamsValidationFailed); } const immoManifest = { @@ -549,10 +515,7 @@ export class ManifestService { return immoManifest; default: - throw new ControlledError( - ErrorJob.HCaptchaInvalidJobType, - HttpStatus.CONFLICT, - ); + throw new ValidationError(ErrorJob.HCaptchaInvalidJobType); } } @@ -673,12 +636,9 @@ export class ManifestService { Object.assign(dtoCheck, manifest); - const validationErrors: ValidationError[] = await validate(dtoCheck); + const validationErrors: ClassValidationError[] = await validate(dtoCheck); if (validationErrors.length > 0) { - throw new ControlledError( - ErrorJob.ManifestValidationFailed, - HttpStatus.NOT_FOUND, - ); + throw new ValidationError(ErrorJob.ManifestValidationFailed); } } diff --git a/packages/apps/job-launcher/server/src/modules/mutex/mutex-manager.service.ts b/packages/apps/job-launcher/server/src/modules/mutex/mutex-manager.service.ts index 850b9d3376..159face8a5 100644 --- a/packages/apps/job-launcher/server/src/modules/mutex/mutex-manager.service.ts +++ b/packages/apps/job-launcher/server/src/modules/mutex/mutex-manager.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common'; -import { Mutex, MutexInterface, withTimeout, E_TIMEOUT } from 'async-mutex'; -import { ControlledError } from '../../common/errors/controlled'; +import { E_TIMEOUT, Mutex, MutexInterface, withTimeout } from 'async-mutex'; +import { ServerError } from '../../common/errors'; @Injectable() export class MutexManagerService implements OnModuleDestroy { @@ -60,9 +60,6 @@ export class MutexManagerService implements OnModuleDestroy { }); return result; } catch (e) { - if (e instanceof ControlledError) { - throw e; - } if (e === E_TIMEOUT) { this.logger.error( `Function execution timed out for ${(key as any).id as string}`, @@ -75,7 +72,7 @@ export class MutexManagerService implements OnModuleDestroy { `Function execution failed for ${(key as any).id as string}`, e, ); - throw new Error( + throw new ServerError( `Function execution failed for ${(key as any).id as string}`, ); } diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts index 0cc824795b..d9ce950b3b 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.controller.ts @@ -4,7 +4,6 @@ import { Delete, Get, Headers, - HttpStatus, Param, Patch, Post, @@ -25,11 +24,14 @@ import { RequestWithUser } from '../../common/types'; import { ChainId } from '@human-protocol/sdk'; import { ErrorPayment } from 'src/common/constants/errors'; import { ServerConfigService } from '../../common/config/server-config.service'; -import { Web3ConfigService } from '../../common/config/web3-config.service'; import { HEADER_SIGNATURE_KEY } from '../../common/constants'; import { TOKEN_ADDRESSES } from '../../common/constants/tokens'; import { ApiKey } from '../../common/decorators'; -import { ControlledError } from '../../common/errors/controlled'; +import { + ConflictError, + ServerError, + ValidationError, +} from '../../common/errors'; import { WhitelistAuthGuard } from '../../common/guards/whitelist.auth'; import { PageDto } from '../../common/pagination/pagination.dto'; import { RateService } from '../rate/rate.service'; @@ -44,11 +46,11 @@ import { PaymentFiatConfirmDto, PaymentFiatCreateDto, PaymentMethodIdDto, + TokenDto, TokensResponseDto, UserBalanceDto, } from './payment.dto'; import { PaymentService } from './payment.service'; -import { TokenDto } from './payment.dto'; @ApiBearerAuth() @UseGuards(JwtAuthGuard) @@ -59,7 +61,6 @@ export class PaymentController { private readonly paymentService: PaymentService, private readonly serverConfigService: ServerConfigService, private readonly rateService: RateService, - private readonly web3ConfigService: Web3ConfigService, ) {} @ApiOperation({ @@ -83,10 +84,7 @@ export class PaymentController { try { return this.paymentService.getUserBalance(req.user.id); } catch { - throw new ControlledError( - ErrorPayment.BalanceCouldNotBeRetrieved, - HttpStatus.UNPROCESSABLE_ENTITY, - ); + throw new ServerError(ErrorPayment.BalanceCouldNotBeRetrieved); } } @@ -152,11 +150,7 @@ export class PaymentController { try { return this.rateService.getRate(data.from, data.to); } catch (e) { - throw new ControlledError( - 'Error getting rates', - HttpStatus.CONFLICT, - e.stack, - ); + throw new ConflictError('Error getting rates', e.stack); } } @@ -454,10 +448,7 @@ export class PaymentController { ): Promise { const tokens = TOKEN_ADDRESSES[chainId]; if (!tokens) { - throw new ControlledError( - ErrorPayment.InvalidChainId, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorPayment.InvalidChainId); } return tokens; diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.service.spec.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.service.spec.ts index 6dfa82c298..4f1d74d264 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.service.spec.ts @@ -1,57 +1,60 @@ -import { ethers } from 'ethers'; +jest.mock('@human-protocol/sdk'); +jest.mock('../../common/utils/signature', () => ({ + verifySignature: jest.fn().mockReturnValue(true), +})); + +import { faker } from '@faker-js/faker/.'; +import { createMock } from '@golevelup/ts-jest'; +import { HMToken__factory } from '@human-protocol/core/typechain-types'; +import { ChainId, NETWORKS } from '@human-protocol/sdk'; +import { HttpService } from '@nestjs/axios'; +import { ConflictException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; +import { ethers } from 'ethers'; import Stripe from 'stripe'; -import { PaymentService } from './payment.service'; -import { PaymentRepository } from './payment.repository'; -import { ConfigService } from '@nestjs/config'; -import { HttpService } from '@nestjs/axios'; -import { createMock } from '@golevelup/ts-jest'; +import { + MOCK_ADDRESS, + MOCK_PAYMENT_ID, + MOCK_SIGNATURE, + MOCK_TRANSACTION_HASH, + mockConfig, +} from '../../../test/constants'; +import { NetworkConfigService } from '../../common/config/network-config.service'; +import { ServerConfigService } from '../../common/config/server-config.service'; +import { StripeConfigService } from '../../common/config/stripe-config.service'; +import { TX_CONFIRMATION_TRESHOLD } from '../../common/constants'; import { ErrorPayment, ErrorPostgres, ErrorSignature, } from '../../common/constants/errors'; +import { SortDirection } from '../../common/enums/collection'; +import { Country } from '../../common/enums/job'; import { + PaymentCurrency, PaymentSortField, PaymentSource, PaymentStatus, PaymentType, StripePaymentStatus, VatType, - PaymentCurrency, } from '../../common/enums/payment'; -import { TX_CONFIRMATION_TRESHOLD } from '../../common/constants'; import { - MOCK_ADDRESS, - MOCK_PAYMENT_ID, - MOCK_SIGNATURE, - MOCK_TRANSACTION_HASH, - mockConfig, -} from '../../../test/constants'; -import { ServerConfigService } from '../../common/config/server-config.service'; -import { Web3Service } from '../web3/web3.service'; -import { HMToken__factory } from '@human-protocol/core/typechain-types'; -import { ChainId, NETWORKS } from '@human-protocol/sdk'; -import { PaymentEntity } from './payment.entity'; + ConflictError, + DatabaseError, + NotFoundError, + ServerError, +} from '../../common/errors'; import { verifySignature } from '../../common/utils/signature'; -import { ConflictException, HttpStatus } from '@nestjs/common'; -import { DatabaseError } from '../../common/errors/database'; -import { StripeConfigService } from '../../common/config/stripe-config.service'; -import { NetworkConfigService } from '../../common/config/network-config.service'; -import { ControlledError } from '../../common/errors/controlled'; +import { JobRepository } from '../job/job.repository'; import { RateService } from '../rate/rate.service'; import { UserRepository } from '../user/user.repository'; -import { JobRepository } from '../job/job.repository'; +import { Web3Service } from '../web3/web3.service'; import { GetPaymentsDto, UserBalanceDto } from './payment.dto'; -import { SortDirection } from '../../common/enums/collection'; -import { Country } from '../../common/enums/job'; -import { faker } from '@faker-js/faker/.'; - -jest.mock('@human-protocol/sdk'); - -jest.mock('../../common/utils/signature', () => ({ - verifySignature: jest.fn().mockReturnValue(true), -})); +import { PaymentEntity } from './payment.entity'; +import { PaymentRepository } from './payment.repository'; +import { PaymentService } from './payment.service'; describe('PaymentService', () => { let stripe: Stripe; @@ -283,10 +286,7 @@ describe('PaymentService', () => { await expect( paymentService.createFiatPayment(user as any, dto), ).rejects.toThrow( - new ControlledError( - ErrorPayment.TransactionAlreadyExists, - HttpStatus.BAD_REQUEST, - ), + new ConflictError(ErrorPayment.TransactionAlreadyExists), ); }); @@ -321,12 +321,7 @@ describe('PaymentService', () => { await expect( paymentService.createFiatPayment(user as any, dto), - ).rejects.toThrow( - new ControlledError( - ErrorPayment.ClientSecretDoesNotExist, - HttpStatus.NOT_FOUND, - ), - ); + ).rejects.toThrow(new ServerError(ErrorPayment.ClientSecretDoesNotExist)); }); }); @@ -395,9 +390,7 @@ describe('PaymentService', () => { await expect( paymentService.confirmFiatPayment(userId, dto), - ).rejects.toThrow( - new ControlledError(ErrorPayment.NotSuccess, HttpStatus.BAD_REQUEST), - ); + ).rejects.toThrow(new ConflictError(ErrorPayment.NotSuccess)); }); it('should handle payment requiring a payment method', async () => { @@ -425,9 +418,7 @@ describe('PaymentService', () => { await expect( paymentService.confirmFiatPayment(userId, dto), - ).rejects.toThrow( - new ControlledError(ErrorPayment.NotSuccess, HttpStatus.BAD_REQUEST), - ); + ).rejects.toThrow(new ConflictError(ErrorPayment.NotSuccess)); }); it('should handle payment status other than succeeded', async () => { @@ -468,9 +459,7 @@ describe('PaymentService', () => { await expect( paymentService.confirmFiatPayment(userId, dto), - ).rejects.toThrow( - new ControlledError(ErrorPayment.NotFound, HttpStatus.NOT_FOUND), - ); + ).rejects.toThrow(new NotFoundError(ErrorPayment.NotFound)); }); }); @@ -600,9 +589,7 @@ describe('PaymentService', () => { await expect( paymentService.createCryptoPayment(userId, dto, MOCK_SIGNATURE), - ).rejects.toThrow( - new ControlledError(ErrorPayment.UnsupportedToken, HttpStatus.CONFLICT), - ); + ).rejects.toThrow(new ConflictError(ErrorPayment.UnsupportedToken)); }); it('should throw a conflict exception if an unsupported token is used', async () => { @@ -641,9 +628,7 @@ describe('PaymentService', () => { await expect( paymentService.createCryptoPayment(userId, dto, MOCK_SIGNATURE), - ).rejects.toThrow( - new ControlledError(ErrorPayment.UnsupportedToken, HttpStatus.CONFLICT), - ); + ).rejects.toThrow(new ConflictError(ErrorPayment.UnsupportedToken)); }); it('should throw a signature error if the signature is wrong', async () => { @@ -668,12 +653,7 @@ describe('PaymentService', () => { await expect( paymentService.createCryptoPayment(userId, dto, MOCK_SIGNATURE), - ).rejects.toThrow( - new ControlledError( - ErrorSignature.SignatureNotVerified, - HttpStatus.CONFLICT, - ), - ); + ).rejects.toThrow(new ConflictError(ErrorSignature.SignatureNotVerified)); }); it('should throw a not found exception if the transaction is not found by hash', async () => { @@ -688,10 +668,7 @@ describe('PaymentService', () => { await expect( paymentService.createCryptoPayment(userId, dto, MOCK_SIGNATURE), ).rejects.toThrow( - new ControlledError( - ErrorPayment.TransactionNotFoundByHash, - HttpStatus.NOT_FOUND, - ), + new NotFoundError(ErrorPayment.TransactionNotFoundByHash), ); }); @@ -714,12 +691,7 @@ describe('PaymentService', () => { await expect( paymentService.createCryptoPayment(userId, dto, MOCK_SIGNATURE), - ).rejects.toThrow( - new ControlledError( - ErrorPayment.InvalidTransactionData, - HttpStatus.NOT_FOUND, - ), - ); + ).rejects.toThrow(new ServerError(ErrorPayment.InvalidTransactionData)); }); it('should throw a not found exception if the transaction has insufficient confirmations', async () => { @@ -757,9 +729,8 @@ describe('PaymentService', () => { await expect( paymentService.createCryptoPayment(userId, dto, MOCK_SIGNATURE), ).rejects.toThrow( - new ControlledError( + new ConflictError( ErrorPayment.TransactionHasNotEnoughAmountOfConfirmations, - HttpStatus.NOT_FOUND, ), ); }); @@ -801,10 +772,7 @@ describe('PaymentService', () => { await expect( paymentService.createCryptoPayment(userId, dto, MOCK_SIGNATURE), ).rejects.toThrow( - new ControlledError( - ErrorPayment.TransactionAlreadyExists, - HttpStatus.BAD_REQUEST, - ), + new ConflictError(ErrorPayment.TransactionAlreadyExists), ); }); }); @@ -923,12 +891,7 @@ describe('PaymentService', () => { await expect( paymentService.createCustomerAndAssignCard(user as any), - ).rejects.toThrow( - new ControlledError( - ErrorPayment.CustomerNotCreated, - HttpStatus.NOT_FOUND, - ), - ); + ).rejects.toThrow(new ServerError(ErrorPayment.CustomerNotCreated)); }); it('should throw a bad request exception if the setup intent creation fails', async () => { @@ -1210,12 +1173,7 @@ describe('PaymentService', () => { await expect( paymentService.deletePaymentMethod(user as any, 'pm_123'), - ).rejects.toThrow( - new ControlledError( - ErrorPayment.PaymentMethodInUse, - HttpStatus.BAD_REQUEST, - ), - ); + ).rejects.toThrow(new ConflictError(ErrorPayment.PaymentMethodInUse)); }); }); @@ -1508,7 +1466,7 @@ describe('PaymentService', () => { retrievePaymentIntentMock.mockResolvedValue(null); await expect(paymentService.getReceipt(paymentId, user)).rejects.toThrow( - new ControlledError(ErrorPayment.NotFound, HttpStatus.NOT_FOUND), + new NotFoundError(ErrorPayment.NotFound), ); }); @@ -1524,7 +1482,7 @@ describe('PaymentService', () => { retrieveChargeMock.mockResolvedValue(null); await expect(paymentService.getReceipt(paymentId, user)).rejects.toThrow( - new ControlledError(ErrorPayment.NotFound, HttpStatus.NOT_FOUND), + new NotFoundError(ErrorPayment.NotFound), ); }); }); @@ -1644,12 +1602,7 @@ describe('PaymentService', () => { await expect( paymentService.createWithdrawalPayment(userId, amount, currency, rate), - ).rejects.toThrow( - new ControlledError( - ErrorPayment.NotEnoughFunds, - HttpStatus.BAD_REQUEST, - ), - ); + ).rejects.toThrow(new ServerError(ErrorPayment.NotEnoughFunds)); }); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts index 412bec9075..e7be0f16e1 100644 --- a/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts +++ b/packages/apps/job-launcher/server/src/modules/payment/payment.service.ts @@ -1,9 +1,29 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { HttpStatus, Injectable, Logger } from '@nestjs/common'; -import Stripe from 'stripe'; +import { + HMToken, + HMToken__factory, +} from '@human-protocol/core/typechain-types'; +import { Injectable, Logger } from '@nestjs/common'; import { ethers, formatUnits } from 'ethers'; +import Stripe from 'stripe'; +import { NetworkConfigService } from '../../common/config/network-config.service'; +import { ServerConfigService } from '../../common/config/server-config.service'; +import { StripeConfigService } from '../../common/config/stripe-config.service'; +import { TX_CONFIRMATION_TRESHOLD } from '../../common/constants'; import { ErrorPayment } from '../../common/constants/errors'; -import { PaymentRepository } from './payment.repository'; +import { CoingeckoTokenId } from '../../common/constants/payment'; +import { + FiatCurrency, + PaymentCurrency, + PaymentSource, + PaymentStatus, + PaymentType, + StripePaymentStatus, + VatType, +} from '../../common/enums/payment'; +import { add, div, eq, lt, mul } from '../../common/utils/decimal'; +import { verifySignature } from '../../common/utils/signature'; +import { Web3Service } from '../web3/web3.service'; import { AddressDto, BillingInfoDto, @@ -18,37 +38,18 @@ import { PaymentRefund, UserBalanceDto, } from './payment.dto'; -import { - FiatCurrency, - PaymentCurrency, - PaymentSource, - PaymentStatus, - PaymentType, - StripePaymentStatus, - VatType, -} from '../../common/enums/payment'; -import { TX_CONFIRMATION_TRESHOLD } from '../../common/constants'; -import { NetworkConfigService } from '../../common/config/network-config.service'; -import { StripeConfigService } from '../../common/config/stripe-config.service'; -import { ServerConfigService } from '../../common/config/server-config.service'; -import { - HMToken, - HMToken__factory, -} from '@human-protocol/core/typechain-types'; -import { Web3Service } from '../web3/web3.service'; -import { CoingeckoTokenId } from '../../common/constants/payment'; -import { div, eq, mul, add, lt } from '../../common/utils/decimal'; -import { verifySignature } from '../../common/utils/signature'; import { PaymentEntity } from './payment.entity'; -import { ControlledError } from '../../common/errors/controlled'; -import { RateService } from '../rate/rate.service'; -import { UserEntity } from '../user/user.entity'; -import { UserRepository } from '../user/user.repository'; -import { JobRepository } from '../job/job.repository'; -import { PageDto } from '../../common/pagination/pagination.dto'; +import { PaymentRepository } from './payment.repository'; + import { TOKEN_ADDRESSES } from '../../common/constants/tokens'; import { EscrowFundToken } from '../../common/enums/job'; +import { ConflictError, NotFoundError, ServerError } from '../../common/errors'; +import { PageDto } from '../../common/pagination/pagination.dto'; import { JobEntity } from '../job/job.entity'; +import { JobRepository } from '../job/job.repository'; +import { RateService } from '../rate/rate.service'; +import { UserEntity } from '../user/user.entity'; +import { UserRepository } from '../user/user.repository'; @Injectable() export class PaymentService { @@ -91,10 +92,7 @@ export class PaymentService { ).id; } catch (error) { this.logger.log(error.message, PaymentService.name); - throw new ControlledError( - ErrorPayment.CustomerNotCreated, - HttpStatus.BAD_REQUEST, - ); + throw new ServerError(ErrorPayment.CustomerNotCreated); } } try { @@ -107,10 +105,7 @@ export class PaymentService { }); } catch (error) { this.logger.log(error.message, PaymentService.name); - throw new ControlledError( - ErrorPayment.CardNotAssigned, - HttpStatus.BAD_REQUEST, - ); + throw new ServerError(ErrorPayment.CardNotAssigned); } // Ensure the SetupIntent contains a client secret for completing the card setup process. @@ -119,10 +114,7 @@ export class PaymentService { ErrorPayment.ClientSecretDoesNotExist, PaymentService.name, ); - throw new ControlledError( - ErrorPayment.ClientSecretDoesNotExist, - HttpStatus.NOT_FOUND, - ); + throw new ServerError(ErrorPayment.ClientSecretDoesNotExist); } return setupIntent.client_secret; @@ -137,10 +129,7 @@ export class PaymentService { if (!setup) { this.logger.log(ErrorPayment.SetupNotFound, PaymentService.name); - throw new ControlledError( - ErrorPayment.SetupNotFound, - HttpStatus.NOT_FOUND, - ); + throw new NotFoundError(ErrorPayment.SetupNotFound); } if (data.defaultCard || !user.stripeCustomerId) { // Update Stripe customer settings to use this payment method by default. @@ -185,10 +174,7 @@ export class PaymentService { ); if (paymentEntity) { - throw new ControlledError( - ErrorPayment.TransactionAlreadyExists, - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError(ErrorPayment.TransactionAlreadyExists); } const newPaymentEntity = new PaymentEntity(); @@ -217,7 +203,7 @@ export class PaymentService { ); if (!paymentData) { - throw new ControlledError(ErrorPayment.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorPayment.NotFound); } const paymentEntity = await this.paymentRepository.findOneByTransaction( @@ -232,7 +218,7 @@ export class PaymentService { !eq(paymentEntity.amount, div(paymentData.amount_received, 100)) || paymentEntity.currency !== paymentData.currency ) { - throw new ControlledError(ErrorPayment.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorPayment.NotFound); } if ( @@ -241,10 +227,7 @@ export class PaymentService { ) { paymentEntity.status = PaymentStatus.FAILED; await this.paymentRepository.updateOne(paymentEntity); - throw new ControlledError( - ErrorPayment.NotSuccess, - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError(ErrorPayment.NotSuccess); } else if (paymentData?.status !== StripePaymentStatus.SUCCEEDED) { return false; // TODO: Handling other cases } @@ -272,28 +255,21 @@ export class PaymentService { ); if (!transaction) { - throw new ControlledError( - ErrorPayment.TransactionNotFoundByHash, - HttpStatus.NOT_FOUND, - ); + throw new NotFoundError(ErrorPayment.TransactionNotFoundByHash); } verifySignature(dto, signature, [transaction.from]); if (!transaction.logs[0] || !transaction.logs[0].data) { - throw new ControlledError( - ErrorPayment.InvalidTransactionData, - HttpStatus.NOT_FOUND, - ); + throw new ServerError(ErrorPayment.InvalidTransactionData); } if ((await transaction.confirmations()) < TX_CONFIRMATION_TRESHOLD) { this.logger.error( `Transaction has ${transaction.confirmations} confirmations instead of ${TX_CONFIRMATION_TRESHOLD}`, ); - throw new ControlledError( + throw new ConflictError( ErrorPayment.TransactionHasNotEnoughAmountOfConfirmations, - HttpStatus.NOT_FOUND, ); } @@ -313,20 +289,14 @@ export class PaymentService { })?.args['_to'], ) !== ethers.hexlify(signer.address) ) { - throw new ControlledError( - ErrorPayment.InvalidRecipient, - HttpStatus.CONFLICT, - ); + throw new ConflictError(ErrorPayment.InvalidRecipient); } const tokenId = (await tokenContract.symbol()).toLowerCase(); const token = TOKEN_ADDRESSES[dto.chainId]?.[tokenId as EscrowFundToken]; if (token?.address !== tokenAddress || !CoingeckoTokenId[tokenId]) { - throw new ControlledError( - ErrorPayment.UnsupportedToken, - HttpStatus.CONFLICT, - ); + throw new ConflictError(ErrorPayment.UnsupportedToken); } const amount = Number( @@ -339,10 +309,7 @@ export class PaymentService { ); if (paymentEntity) { - throw new ControlledError( - ErrorPayment.TransactionAlreadyExists, - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError(ErrorPayment.TransactionAlreadyExists); } const rate = await this.rateService.getRate(tokenId, FiatCurrency.USD); @@ -435,10 +402,7 @@ export class PaymentService { invoice = await this.stripe.invoices.finalizeInvoice(invoice.id); if (!invoice.payment_intent) { - throw new ControlledError( - ErrorPayment.IntentNotCreated, - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new ServerError(ErrorPayment.IntentNotCreated); } return invoice; @@ -463,20 +427,14 @@ export class PaymentService { }); } } catch { - throw new ControlledError( - ErrorPayment.PaymentMethodAssociationFailed, - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new ServerError(ErrorPayment.PaymentMethodAssociationFailed); } const paymentIntent = await this.stripe.paymentIntents.retrieve(paymentIntentId); if (!paymentIntent?.client_secret) { - throw new ControlledError( - ErrorPayment.ClientSecretDoesNotExist, - HttpStatus.NOT_FOUND, - ); + throw new ServerError(ErrorPayment.ClientSecretDoesNotExist); } return paymentIntent; @@ -489,10 +447,7 @@ export class PaymentService { const user = await this.userRepository.findById(job.userId); if (!user) { this.logger.log(ErrorPayment.CustomerNotFound, PaymentService.name); - throw new ControlledError( - ErrorPayment.CustomerNotFound, - HttpStatus.BAD_REQUEST, - ); + throw new NotFoundError(ErrorPayment.CustomerNotFound); } const amountInCents = Math.ceil(mul(amount, 100)); @@ -508,10 +463,7 @@ export class PaymentService { ); if (!defaultPaymentMethod) { - throw new ControlledError( - ErrorPayment.NotDefaultPaymentMethod, - HttpStatus.INTERNAL_SERVER_ERROR, - ); + throw new ServerError(ErrorPayment.NotDefaultPaymentMethod); } const paymentIntent = await this.handleStripePaymentIntent( @@ -556,10 +508,7 @@ export class PaymentService { // Check if the user has enough balance const userBalance = await this.getUserBalanceByCurrency(userId, currency); if (lt(userBalance, amount)) { - throw new ControlledError( - ErrorPayment.NotEnoughFunds, - HttpStatus.BAD_REQUEST, - ); + throw new ServerError(ErrorPayment.NotEnoughFunds); } const paymentEntity = new PaymentEntity(); @@ -618,10 +567,7 @@ export class PaymentService { (await this.getDefaultPaymentMethod(user.stripeCustomerId)) && (await this.isPaymentMethodInUse(user.id)) ) { - throw new ControlledError( - ErrorPayment.PaymentMethodInUse, - HttpStatus.BAD_REQUEST, - ); + throw new ConflictError(ErrorPayment.PaymentMethodInUse); } // Detach the payment method from the user's account @@ -663,10 +609,7 @@ export class PaymentService { updateBillingInfoDto: BillingInfoDto, ) { if (!user.stripeCustomerId) { - throw new ControlledError( - ErrorPayment.CustomerNotFound, - HttpStatus.BAD_REQUEST, - ); + throw new NotFoundError(ErrorPayment.CustomerNotFound); } // If the VAT or VAT type has changed, update it in Stripe const existingTaxIds = await this.stripe.customers.listTaxIds( @@ -714,10 +657,7 @@ export class PaymentService { async getDefaultPaymentMethod(customerId: string): Promise { if (!customerId) { - throw new ControlledError( - ErrorPayment.CustomerNotFound, - HttpStatus.BAD_REQUEST, - ); + throw new NotFoundError(ErrorPayment.CustomerNotFound); } // Retrieve the customer from Stripe and return the default payment method @@ -770,7 +710,7 @@ export class PaymentService { const paymentIntent = await this.stripe.paymentIntents.retrieve(paymentId); if (!paymentIntent || paymentIntent.customer !== user.stripeCustomerId) { - throw new ControlledError(ErrorPayment.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorPayment.NotFound); } // Retrieve the charge for the payment intent and ensure it has a receipt URL @@ -778,7 +718,7 @@ export class PaymentService { paymentIntent.latest_charge as string, ); if (!charge || !charge.receipt_url) { - throw new ControlledError(ErrorPayment.NotFound, HttpStatus.NOT_FOUND); + throw new NotFoundError(ErrorPayment.NotFound); } return charge.receipt_url; diff --git a/packages/apps/job-launcher/server/src/modules/qualification/qualification.service.spec.ts b/packages/apps/job-launcher/server/src/modules/qualification/qualification.service.spec.ts index f214d95a69..5136a5e6af 100644 --- a/packages/apps/job-launcher/server/src/modules/qualification/qualification.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/qualification/qualification.service.spec.ts @@ -1,27 +1,26 @@ -import { Test } from '@nestjs/testing'; -import { QualificationService } from './qualification.service'; -import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { ConfigService } from '@nestjs/config'; +jest.mock('@human-protocol/sdk', () => ({ + ...jest.requireActual('@human-protocol/sdk'), + KVStoreUtils: { + get: jest.fn(), + }, +})); + +import { ChainId, KVStoreUtils } from '@human-protocol/sdk'; import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; import { of, throwError } from 'rxjs'; -import { ChainId, KVStoreUtils } from '@human-protocol/sdk'; import { MOCK_REPUTATION_ORACLE_URL, MOCK_WEB3_RPC_URL, mockConfig, } from '../../../test/constants'; -import { ControlledError } from '../../common/errors/controlled'; -import { ErrorQualification, ErrorWeb3 } from '../../common/constants/errors'; -import { HttpStatus } from '@nestjs/common'; import { NetworkConfigService } from '../../common/config/network-config.service'; +import { Web3ConfigService } from '../../common/config/web3-config.service'; +import { ErrorQualification, ErrorWeb3 } from '../../common/constants/errors'; +import { ServerError, ValidationError } from '../../common/errors'; import { Web3Service } from '../web3/web3.service'; - -jest.mock('@human-protocol/sdk', () => ({ - ...jest.requireActual('@human-protocol/sdk'), - KVStoreUtils: { - get: jest.fn(), - }, -})); +import { QualificationService } from './qualification.service'; describe.only('QualificationService', () => { let qualificationService: QualificationService, httpService: HttpService; @@ -97,37 +96,31 @@ describe.only('QualificationService', () => { expect(result).toEqual(qualifications); }); - it('should throw a ControlledError when KVStoreUtils.get fails', async () => { + it('should throw a ServerError when KVStoreUtils.get fails', async () => { (KVStoreUtils.get as any).mockRejectedValue(new Error('KV store error')); await expect( qualificationService.getQualifications(ChainId.LOCALHOST), - ).rejects.toThrow( - new ControlledError( - ErrorWeb3.ReputationOracleUrlNotSet, - HttpStatus.BAD_REQUEST, - ), - ); + ).rejects.toThrow(new ServerError(ErrorWeb3.ReputationOracleUrlNotSet)); }); - it('should throw a ControlledError when HTTP request fails', async () => { + it('should throw a ServerError when HTTP request fails', async () => { (KVStoreUtils.get as any).mockResolvedValue(MOCK_REPUTATION_ORACLE_URL); jest .spyOn(httpService, 'get') - .mockImplementation(() => throwError(new Error('HTTP error')) as any); + .mockImplementation( + () => throwError(() => new Error('HTTP error')) as any, + ); await expect( qualificationService.getQualifications(ChainId.LOCALHOST), ).rejects.toThrow( - new ControlledError( - ErrorQualification.FailedToFetchQualifications, - HttpStatus.BAD_REQUEST, - ), + new ServerError(ErrorQualification.FailedToFetchQualifications), ); }); - it('should throw a ControlledError when invalid chainId', async () => { + it('should throw a ValidationError when invalid chainId', async () => { (KVStoreUtils.get as any).mockResolvedValue(MOCK_REPUTATION_ORACLE_URL); jest @@ -136,9 +129,7 @@ describe.only('QualificationService', () => { await expect( qualificationService.getQualifications(ChainId.MAINNET), - ).rejects.toThrow( - new ControlledError(ErrorWeb3.InvalidChainId, HttpStatus.BAD_REQUEST), - ); + ).rejects.toThrow(new ValidationError(ErrorWeb3.InvalidChainId)); }); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/qualification/qualification.service.ts b/packages/apps/job-launcher/server/src/modules/qualification/qualification.service.ts index 9e51d255fc..00837e962a 100644 --- a/packages/apps/job-launcher/server/src/modules/qualification/qualification.service.ts +++ b/packages/apps/job-launcher/server/src/modules/qualification/qualification.service.ts @@ -1,12 +1,12 @@ -import { HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { QualificationDto } from './qualification.dto'; -import { firstValueFrom } from 'rxjs'; +import { ChainId, KVStoreKeys, KVStoreUtils } from '@human-protocol/sdk'; import { HttpService } from '@nestjs/axios'; +import { Injectable, Logger } from '@nestjs/common'; +import { firstValueFrom } from 'rxjs'; import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { ControlledError } from '../../common/errors/controlled'; import { ErrorQualification, ErrorWeb3 } from '../../common/constants/errors'; -import { ChainId, KVStoreKeys, KVStoreUtils } from '@human-protocol/sdk'; +import { ServerError } from '../../common/errors'; import { Web3Service } from '../web3/web3.service'; +import { QualificationDto } from './qualification.dto'; @Injectable() export class QualificationService { @@ -21,26 +21,22 @@ export class QualificationService { public async getQualifications( chainId: ChainId, ): Promise { - try { - let reputationOracleUrl = ''; + let reputationOracleUrl = ''; + this.web3Service.validateChainId(chainId); - this.web3Service.validateChainId(chainId); - - try { - reputationOracleUrl = await KVStoreUtils.get( - chainId, - this.web3ConfigService.reputationOracleAddress, - KVStoreKeys.url, - ); - } catch {} + try { + reputationOracleUrl = await KVStoreUtils.get( + chainId, + this.web3ConfigService.reputationOracleAddress, + KVStoreKeys.url, + ); + } catch {} - if (!reputationOracleUrl || reputationOracleUrl === '') { - throw new ControlledError( - ErrorWeb3.ReputationOracleUrlNotSet, - HttpStatus.BAD_REQUEST, - ); - } + if (!reputationOracleUrl || reputationOracleUrl === '') { + throw new ServerError(ErrorWeb3.ReputationOracleUrlNotSet); + } + try { const { data } = await firstValueFrom( this.httpService.get( `${reputationOracleUrl}/qualifications`, @@ -49,14 +45,10 @@ export class QualificationService { return data; } catch (error) { - if (error instanceof ControlledError) { - throw error; - } else { - throw new ControlledError( - ErrorQualification.FailedToFetchQualifications, - HttpStatus.BAD_REQUEST, - ); - } + this.logger.error( + `Error fetching qualifications from reputation oracle: ${error}`, + ); + throw new ServerError(ErrorQualification.FailedToFetchQualifications); } } } diff --git a/packages/apps/job-launcher/server/src/modules/rate/rate.service.spec.ts b/packages/apps/job-launcher/server/src/modules/rate/rate.service.spec.ts index fb2e3db575..c473f0dffa 100644 --- a/packages/apps/job-launcher/server/src/modules/rate/rate.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/rate/rate.service.spec.ts @@ -1,13 +1,12 @@ import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { of } from 'rxjs'; -import { RateService } from './rate.service'; -import { ControlledError } from '../../common/errors/controlled'; -import { ErrorCurrency } from '../../common/constants/errors'; -import { ServerConfigService } from '../../common/config/server-config.service'; -import { HttpStatus } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; import { mockConfig } from '../../../test/constants'; +import { ServerConfigService } from '../../common/config/server-config.service'; +import { ErrorCurrency } from '../../common/constants/errors'; +import { NotFoundError } from '../../common/errors'; +import { RateService } from './rate.service'; describe('RateService', () => { let service: RateService; @@ -89,8 +88,8 @@ describe('RateService', () => { }) as any, ); - await expect(service.getRate(from, to)).rejects.toThrowError( - new ControlledError(ErrorCurrency.PairNotFound, HttpStatus.NOT_FOUND), + await expect(service.getRate(from, to)).rejects.toThrow( + new NotFoundError(ErrorCurrency.PairNotFound), ); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/rate/rate.service.ts b/packages/apps/job-launcher/server/src/modules/rate/rate.service.ts index 2d15ed2d57..8c5b008e62 100644 --- a/packages/apps/job-launcher/server/src/modules/rate/rate.service.ts +++ b/packages/apps/job-launcher/server/src/modules/rate/rate.service.ts @@ -1,12 +1,12 @@ import { HttpService } from '@nestjs/axios'; -import { HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { firstValueFrom } from 'rxjs'; import { ServerConfigService } from '../../common/config/server-config.service'; import { COINGECKO_API_URL } from '../../common/constants'; import { ErrorCurrency } from '../../common/constants/errors'; import { CoingeckoTokenId } from '../../common/constants/payment'; -import { ControlledError } from '../../common/errors/controlled'; import { EscrowFundToken } from '../../common/enums/job'; +import { NotFoundError } from '../../common/errors'; @Injectable() export class RateService { @@ -67,10 +67,7 @@ export class RateService { )) as any; if (!data[coingeckoFrom] || !data[coingeckoFrom][coingeckoTo]) { - throw new ControlledError( - ErrorCurrency.PairNotFound, - HttpStatus.NOT_FOUND, - ); + throw new NotFoundError(ErrorCurrency.PairNotFound); } const rate = data[coingeckoFrom][coingeckoTo]; const finalRate = reversed ? 1 / rate : rate; @@ -80,10 +77,7 @@ export class RateService { return finalRate; } catch (error) { this.logger.error(error); - throw new ControlledError( - ErrorCurrency.PairNotFound, - HttpStatus.NOT_FOUND, - ); + throw new NotFoundError(ErrorCurrency.PairNotFound); } } } diff --git a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.spec.ts b/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.spec.ts index b08b0a94b1..9893a6d342 100644 --- a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.spec.ts @@ -1,18 +1,3 @@ -import { Test } from '@nestjs/testing'; - -import { RoutingProtocolService } from './routing-protocol.service'; -import { ChainId, Role } from '@human-protocol/sdk'; -import { MOCK_REPUTATION_ORACLE_1, mockConfig } from '../../../test/constants'; -import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { NetworkConfigService } from '../../common/config/network-config.service'; -import { Web3Service } from '../web3/web3.service'; -import { ConfigService } from '@nestjs/config'; -import { ControlledError } from '../../common/errors/controlled'; -import { FortuneJobType } from '../../common/enums/job'; -import { ErrorRoutingProtocol } from '../../common/constants/errors'; -import { HttpStatus } from '@nestjs/common'; -import { hashString } from '../../common/utils'; - jest.mock('../../common/utils', () => ({ ...jest.requireActual('../../common/utils'), hashString: jest.fn(), @@ -25,6 +10,19 @@ jest.mock('@human-protocol/sdk', () => ({ }, })); +import { ChainId, Role } from '@human-protocol/sdk'; +import { ConfigService } from '@nestjs/config'; +import { Test } from '@nestjs/testing'; +import { MOCK_REPUTATION_ORACLE_1, mockConfig } from '../../../test/constants'; +import { NetworkConfigService } from '../../common/config/network-config.service'; +import { Web3ConfigService } from '../../common/config/web3-config.service'; +import { ErrorRoutingProtocol } from '../../common/constants/errors'; +import { FortuneJobType } from '../../common/enums/job'; +import { ServerError } from '../../common/errors'; +import { hashString } from '../../common/utils'; +import { Web3Service } from '../web3/web3.service'; +import { RoutingProtocolService } from './routing-protocol.service'; + describe('RoutingProtocolService', () => { let web3Service: Web3Service; let routingProtocolService: RoutingProtocolService; @@ -422,10 +420,7 @@ describe('RoutingProtocolService', () => { invalidReputationOracle, ), ).rejects.toThrow( - new ControlledError( - ErrorRoutingProtocol.ReputationOracleNotFound, - HttpStatus.NOT_FOUND, - ), + new ServerError(ErrorRoutingProtocol.ReputationOracleNotFound), ); }); @@ -454,10 +449,7 @@ describe('RoutingProtocolService', () => { 'invalidExchangeOracle', ), ).rejects.toThrow( - new ControlledError( - ErrorRoutingProtocol.ExchangeOracleNotFound, - HttpStatus.NOT_FOUND, - ), + new ServerError(ErrorRoutingProtocol.ExchangeOracleNotFound), ); }); @@ -487,10 +479,7 @@ describe('RoutingProtocolService', () => { 'invalidRecordingOracle', ), ).rejects.toThrow( - new ControlledError( - ErrorRoutingProtocol.RecordingOracleNotFound, - HttpStatus.NOT_FOUND, - ), + new ServerError(ErrorRoutingProtocol.RecordingOracleNotFound), ); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.ts b/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.ts index b682367678..f57657182b 100644 --- a/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.ts +++ b/packages/apps/job-launcher/server/src/modules/routing-protocol/routing-protocol.service.ts @@ -1,8 +1,7 @@ -import { HttpStatus, Injectable, Logger } from '@nestjs/common'; import { ChainId, Role } from '@human-protocol/sdk'; -import { Web3Service } from '../web3/web3.service'; +import { Injectable, Logger } from '@nestjs/common'; +import { NetworkConfigService } from '../../common/config/network-config.service'; import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { hashString } from '../../common/utils'; import { ErrorRoutingProtocol } from '../../common/constants/errors'; import { AudinoJobType, @@ -10,8 +9,9 @@ import { HCaptchaJobType, JobRequestType, } from '../../common/enums/job'; -import { ControlledError } from '../../common/errors/controlled'; -import { NetworkConfigService } from '../../common/config/network-config.service'; +import { ServerError } from '../../common/errors'; +import { hashString } from '../../common/utils'; +import { Web3Service } from '../web3/web3.service'; import { OracleHash, OracleIndex, @@ -217,10 +217,7 @@ export class RoutingProtocolService { .map((address) => address.trim()); if (!reputationOracles.includes(reputationOracle)) { - throw new ControlledError( - ErrorRoutingProtocol.ReputationOracleNotFound, - HttpStatus.NOT_FOUND, - ); + throw new ServerError(ErrorRoutingProtocol.ReputationOracleNotFound); } const availableOracles = await this.web3Service.findAvailableOracles( @@ -237,10 +234,7 @@ export class RoutingProtocolService { Role.ExchangeOracle, ) ) { - throw new ControlledError( - ErrorRoutingProtocol.ExchangeOracleNotFound, - HttpStatus.NOT_FOUND, - ); + throw new ServerError(ErrorRoutingProtocol.ExchangeOracleNotFound); } if ( @@ -251,10 +245,7 @@ export class RoutingProtocolService { Role.RecordingOracle, ) ) { - throw new ControlledError( - ErrorRoutingProtocol.RecordingOracleNotFound, - HttpStatus.NOT_FOUND, - ); + throw new ServerError(ErrorRoutingProtocol.RecordingOracleNotFound); } } diff --git a/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.spec.ts b/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.spec.ts index 90a0d4c967..91b701f986 100644 --- a/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.spec.ts @@ -1,8 +1,6 @@ import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; -import { SendGridService } from './sendgrid.service'; import { MailService } from '@sendgrid/mail'; -import { ErrorSendGrid } from '../../common/constants/errors'; import { MOCK_SENDGRID_API_KEY, MOCK_SENDGRID_FROM_EMAIL, @@ -10,8 +8,9 @@ import { mockConfig, } from '../../../test/constants'; import { SendgridConfigService } from '../../common/config/sendgrid-config.service'; -import { ControlledError } from '../../common/errors/controlled'; -import { HttpStatus } from '@nestjs/common'; +import { ErrorSendGrid } from '../../common/constants/errors'; +import { ConflictError, ServerError } from '../../common/errors'; +import { SendGridService } from './sendgrid.service'; describe('SendGridService', () => { let sendGridService: SendGridService; @@ -102,9 +101,7 @@ describe('SendGridService', () => { text: 'and easy to do anywhere, even with Node.js', html: 'and easy to do anywhere, even with Node.js', }), - ).rejects.toThrow( - new ControlledError(ErrorSendGrid.EmailNotSent, HttpStatus.BAD_REQUEST), - ); + ).rejects.toThrow(new ServerError(ErrorSendGrid.EmailNotSent)); }); }); @@ -132,9 +129,7 @@ describe('SendGridService', () => { mailService, configService as any, ); - }).toThrow( - new ControlledError(ErrorSendGrid.InvalidApiKey, HttpStatus.CONFLICT), - ); + }).toThrow(new ConflictError(ErrorSendGrid.InvalidApiKey)); }); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.ts b/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.ts index bcadbfc8d4..5a5c9bd71f 100644 --- a/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.ts +++ b/packages/apps/job-launcher/server/src/modules/sendgrid/sendgrid.service.ts @@ -1,12 +1,12 @@ -import { HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { MailDataRequired, MailService } from '@sendgrid/mail'; +import { SendgridConfigService } from '../../common/config/sendgrid-config.service'; import { SENDGRID_API_KEY_DISABLED, SENDGRID_API_KEY_REGEX, } from '../../common/constants'; import { ErrorSendGrid } from '../../common/constants/errors'; -import { SendgridConfigService } from '../../common/config/sendgrid-config.service'; -import { ControlledError } from '../../common/errors/controlled'; +import { ConflictError, ServerError } from '../../common/errors'; @Injectable() export class SendGridService { @@ -24,10 +24,7 @@ export class SendGridService { } if (!SENDGRID_API_KEY_REGEX.test(this.sendgridConfigService.apiKey)) { - throw new ControlledError( - ErrorSendGrid.InvalidApiKey, - HttpStatus.CONFLICT, - ); + throw new ConflictError(ErrorSendGrid.InvalidApiKey); } this.mailService.setApiKey(this.sendgridConfigService.apiKey); @@ -61,10 +58,7 @@ export class SendGridService { return; } catch (error) { this.logger.error(error, SendGridService.name); - throw new ControlledError( - ErrorSendGrid.EmailNotSent, - HttpStatus.BAD_REQUEST, - ); + throw new ServerError(ErrorSendGrid.EmailNotSent); } } } diff --git a/packages/apps/job-launcher/server/src/modules/storage/storage.errors.ts b/packages/apps/job-launcher/server/src/modules/storage/storage.errors.ts deleted file mode 100644 index b4eecc4ce4..0000000000 --- a/packages/apps/job-launcher/server/src/modules/storage/storage.errors.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BaseError } from '../../common/errors/base'; - -export class FileDownloadError extends BaseError { - public readonly location: string; - - constructor(location: string, cause?: unknown) { - super('Failed to download file', cause); - - this.location = location; - } -} - -export class InvalidFileUrl extends FileDownloadError { - constructor(url: string) { - super(url); - this.message = 'Invalid file URL'; - } -} - -export class FileNotFoundError extends FileDownloadError { - constructor(location: string) { - super(location); - this.message = 'File not found'; - } -} diff --git a/packages/apps/job-launcher/server/src/modules/storage/storage.service.spec.ts b/packages/apps/job-launcher/server/src/modules/storage/storage.service.spec.ts index 9ac3883148..d751cc26dd 100644 --- a/packages/apps/job-launcher/server/src/modules/storage/storage.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/storage/storage.service.spec.ts @@ -14,10 +14,11 @@ jest.mock('minio', () => { jest.mock('axios'); -import { Encryption, EncryptionUtils, HttpStatus } from '@human-protocol/sdk'; +import { Encryption, EncryptionUtils } from '@human-protocol/sdk'; import { ConfigModule, ConfigService, registerAs } from '@nestjs/config'; import { Test } from '@nestjs/testing'; import axios from 'axios'; +import stringify from 'json-stable-stringify'; import { MOCK_FILE_URL, MOCK_MANIFEST, @@ -31,18 +32,12 @@ import { MOCK_S3_USE_SSL, mockConfig, } from '../../../test/constants'; -import { StorageService } from './storage.service'; -import stringify from 'json-stable-stringify'; -import { ErrorBucket } from '../../common/constants/errors'; -import { hashString } from '../../common/utils'; -import { ContentType } from '../../common/enums/storage'; import { S3ConfigService } from '../../common/config/s3-config.service'; -import { ControlledError } from '../../common/errors/controlled'; -import { - FileDownloadError, - FileNotFoundError, - InvalidFileUrl, -} from './storage.errors'; +import { ErrorBucket, ErrorStorage } from '../../common/constants/errors'; +import { ContentType } from '../../common/enums/storage'; +import { ServerError, ValidationError } from '../../common/errors'; +import { hashString } from '../../common/utils'; +import { StorageService } from './storage.service'; describe('StorageService', () => { let storageService: StorageService; @@ -98,7 +93,10 @@ describe('StorageService', () => { thrownError = error; } - expect(thrownError).toBeInstanceOf(InvalidFileUrl); + expect(thrownError).toBeInstanceOf(ValidationError); + expect(thrownError.message).toContain( + `${ErrorStorage.InvalidUrl}: ${url}`, + ); }, ); @@ -122,11 +120,13 @@ describe('StorageService', () => { thrownError = error; } - expect(thrownError).toBeInstanceOf(FileNotFoundError); - expect(thrownError.location).toBe(testUrl); + expect(thrownError).toBeInstanceOf(ServerError); + expect(thrownError.message).toContain( + `${ErrorStorage.NotFound}: ${testUrl}`, + ); }); - it('throws if netrowk error', async () => { + it('throws if network error', async () => { const testUrl = 'https://network-error.io'; const testError = new Error('ECONNRESET :443'); (axios.get as jest.Mock).mockImplementationOnce((url) => { @@ -145,9 +145,10 @@ describe('StorageService', () => { thrownError = error; } - expect(thrownError).toBeInstanceOf(FileDownloadError); - expect(thrownError.location).toBe(testUrl); - expect(thrownError.cause).toBe(testError); + expect(thrownError).toBeInstanceOf(ServerError); + expect(thrownError.message).toContain( + `${ErrorStorage.FailedToDownload}: ${testUrl}`, + ); }); it('returns response as buffer', async () => { @@ -203,9 +204,7 @@ describe('StorageService', () => { await expect( storageService.uploadJsonLikeData(MOCK_MANIFEST), - ).rejects.toThrow( - new ControlledError(ErrorBucket.NotExist, HttpStatus.BAD_REQUEST), - ); + ).rejects.toThrow(new ServerError(ErrorBucket.NotExist)); }); it('should fail if the file cannot be uploaded', async () => { @@ -218,9 +217,7 @@ describe('StorageService', () => { await expect( storageService.uploadJsonLikeData(MOCK_MANIFEST), - ).rejects.toThrow( - new ControlledError('File not uploaded', HttpStatus.BAD_REQUEST), - ); + ).rejects.toThrow(new ServerError(ErrorStorage.FileNotUploaded)); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts b/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts index dae1d7218f..972d094a34 100644 --- a/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts +++ b/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts @@ -1,19 +1,14 @@ import { Encryption, EncryptionUtils } from '@human-protocol/sdk'; import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import axios from 'axios'; -import * as Minio from 'minio'; import stringify from 'json-stable-stringify'; -import { ErrorBucket } from '../../common/constants/errors'; +import * as Minio from 'minio'; +import { S3ConfigService } from '../../common/config/s3-config.service'; +import { ErrorBucket, ErrorStorage } from '../../common/constants/errors'; import { ContentType, Extension } from '../../common/enums/storage'; +import { ServerError, ValidationError } from '../../common/errors'; import { UploadedFile } from '../../common/interfaces'; -import { S3ConfigService } from '../../common/config/s3-config.service'; -import { ControlledError } from '../../common/errors/controlled'; import { hashString } from '../../common/utils'; -import { - FileDownloadError, - FileNotFoundError, - InvalidFileUrl, -} from './storage.errors'; @Injectable() export class StorageService { @@ -43,7 +38,7 @@ export class StorageService { public static async downloadFileFromUrl(url: string): Promise { if (!this.isValidUrl(url)) { - throw new InvalidFileUrl(url); + throw new ValidationError(`${ErrorStorage.InvalidUrl}: ${url}`); } try { @@ -54,9 +49,9 @@ export class StorageService { return Buffer.from(data); } catch (error) { if (error.response?.status === HttpStatus.NOT_FOUND) { - throw new FileNotFoundError(url); + throw new ServerError(`${ErrorStorage.NotFound}: ${url}`); } - throw new FileDownloadError(url, error.cause || error.message); + throw new ServerError(`${ErrorStorage.FailedToDownload}: ${url}`); } } @@ -106,7 +101,7 @@ export class StorageService { data: string | object, ): Promise { if (!(await this.minioClient.bucketExists(this.s3ConfigService.bucket))) { - throw new ControlledError(ErrorBucket.NotExist, HttpStatus.BAD_REQUEST); + throw new ServerError(ErrorBucket.NotExist); } let fileContents: string; @@ -141,7 +136,7 @@ export class StorageService { hash, }; } catch (_error) { - throw new ControlledError('File not uploaded', HttpStatus.BAD_REQUEST); + throw new ServerError(ErrorStorage.FileNotUploaded); } } } diff --git a/packages/apps/job-launcher/server/src/modules/user/user.service.spec.ts b/packages/apps/job-launcher/server/src/modules/user/user.service.spec.ts index 3aa229aad5..542c5f4e96 100644 --- a/packages/apps/job-launcher/server/src/modules/user/user.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/user/user.service.spec.ts @@ -1,3 +1,5 @@ +jest.mock('@human-protocol/sdk'); + import { createMock } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; @@ -8,8 +10,6 @@ import { UserEntity } from './user.entity'; import { UserRepository } from './user.repository'; import { UserService } from './user.service'; -jest.mock('@human-protocol/sdk'); - describe('UserService', () => { let userService: UserService; let userRepository: UserRepository; diff --git a/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts b/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts index ba65357123..96daa6a720 100644 --- a/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/web3/web3.service.spec.ts @@ -1,9 +1,11 @@ import { ChainId, OperatorUtils, Role } from '@human-protocol/sdk'; import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; +import { NetworkConfigService } from '../../common/config/network-config.service'; +import { Web3ConfigService } from '../../common/config/web3-config.service'; import { ErrorWeb3 } from '../../common/constants/errors'; import { Web3Env } from '../../common/enums/web3'; -import { Web3Service } from './web3.service'; +import { ConflictError, ValidationError } from '../../common/errors'; import { MOCK_ADDRESS, MOCK_EXCHANGE_ORACLE_URL, @@ -11,11 +13,8 @@ import { MOCK_REPUTATION_ORACLES, mockConfig, } from './../../../test/constants'; -import { NetworkConfigService } from '../../common/config/network-config.service'; -import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { ControlledError } from '../../common/errors/controlled'; -import { HttpStatus } from '@nestjs/common'; import { OracleDataDto } from './web3.dto'; +import { Web3Service } from './web3.service'; jest.mock('@human-protocol/sdk', () => { const actualSdk = jest.requireActual('@human-protocol/sdk'); @@ -70,9 +69,7 @@ describe('Web3Service', () => { new Web3ConfigService(configService), new NetworkConfigService(configService), ), - ).toThrow( - new ControlledError(ErrorWeb3.NoValidNetworks, HttpStatus.BAD_REQUEST), - ); + ).toThrow(new Error(ErrorWeb3.NoValidNetworks)); }); }); @@ -92,7 +89,7 @@ describe('Web3Service', () => { const invalidChainId = ChainId.POLYGON; expect(() => web3Service.getSigner(invalidChainId)).toThrow( - new ControlledError(ErrorWeb3.InvalidChainId, HttpStatus.BAD_REQUEST), + new ValidationError(ErrorWeb3.InvalidChainId), ); }); }); @@ -137,9 +134,7 @@ describe('Web3Service', () => { await expect( web3Service.calculateGasPrice(ChainId.POLYGON_AMOY), - ).rejects.toThrow( - new ControlledError(ErrorWeb3.GasPriceError, HttpStatus.CONFLICT), - ); + ).rejects.toThrow(new ConflictError(ErrorWeb3.GasPriceError)); }); }); @@ -153,7 +148,7 @@ describe('Web3Service', () => { it('should throw an error for an invalid chainId', () => { const invalidChainId = ChainId.POLYGON; expect(() => web3Service.validateChainId(invalidChainId)).toThrow( - new ControlledError(ErrorWeb3.InvalidChainId, HttpStatus.BAD_REQUEST), + new ValidationError(ErrorWeb3.InvalidChainId), ); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts b/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts index 548e6d6a58..b05e971802 100644 --- a/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts +++ b/packages/apps/job-launcher/server/src/modules/web3/web3.service.ts @@ -1,11 +1,11 @@ -import { HttpStatus, Injectable, Logger } from '@nestjs/common'; +import { ChainId, OperatorUtils, Role } from '@human-protocol/sdk'; +import { Injectable, Logger } from '@nestjs/common'; import { Wallet, ethers } from 'ethers'; import { NetworkConfigService } from '../../common/config/network-config.service'; import { Web3ConfigService } from '../../common/config/web3-config.service'; import { ErrorWeb3 } from '../../common/constants/errors'; -import { ControlledError } from '../../common/errors/controlled'; +import { ConflictError, ValidationError } from '../../common/errors'; import { AvailableOraclesDto, OracleDataDto } from './web3.dto'; -import { ChainId, OperatorUtils, Role } from '@human-protocol/sdk'; @Injectable() export class Web3Service { @@ -20,10 +20,7 @@ export class Web3Service { const privateKey = this.web3ConfigService.privateKey; if (!this.networkConfigService.networks.length) { - throw new ControlledError( - ErrorWeb3.NoValidNetworks, - HttpStatus.BAD_REQUEST, - ); + throw new Error(ErrorWeb3.NoValidNetworks); } for (const network of this.networkConfigService.networks) { @@ -39,10 +36,7 @@ export class Web3Service { public validateChainId(chainId: number): void { if (!this.signers[chainId]) { - throw new ControlledError( - ErrorWeb3.InvalidChainId, - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError(ErrorWeb3.InvalidChainId); } } @@ -54,7 +48,7 @@ export class Web3Service { if (gasPrice) { return gasPrice * BigInt(multiplier); } - throw new ControlledError(ErrorWeb3.GasPriceError, HttpStatus.CONFLICT); + throw new ConflictError(ErrorWeb3.GasPriceError); } public getOperatorAddress(): string { diff --git a/packages/apps/job-launcher/server/src/modules/webhook/webhook.controller.spec.ts b/packages/apps/job-launcher/server/src/modules/webhook/webhook.controller.spec.ts index ef91574786..35c82ff14d 100644 --- a/packages/apps/job-launcher/server/src/modules/webhook/webhook.controller.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/webhook/webhook.controller.spec.ts @@ -1,16 +1,11 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { WebhookController } from './webhook.controller'; -import { WebhookService } from './webhook.service'; -import { WebhookRepository } from './webhook.repository'; -import { Web3Service } from '../web3/web3.service'; -import { ConfigService } from '@nestjs/config'; -import { HttpService } from '@nestjs/axios'; -import { JobService } from '../job/job.service'; -import { EventType } from '../../common/enums/webhook'; -import { ChainId } from '@human-protocol/sdk'; -import { WebhookDataDto } from './webhook.dto'; +jest.mock('@human-protocol/sdk'); + import { createMock } from '@golevelup/ts-jest'; -import { BadRequestException, HttpStatus } from '@nestjs/common'; +import { ChainId } from '@human-protocol/sdk'; +import { HttpService } from '@nestjs/axios'; +import { BadRequestException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; import { MOCK_ADDRESS, MOCK_CVAT_JOB_SIZE, @@ -19,6 +14,7 @@ import { MOCK_CVAT_VAL_SIZE, MOCK_EXPIRES_IN, MOCK_HCAPTCHA_SITE_KEY, + MOCK_MAX_RETRY_COUNT, MOCK_PGP_PASSPHRASE, MOCK_PGP_PRIVATE_KEY, MOCK_PRIVATE_KEY, @@ -34,14 +30,18 @@ import { MOCK_STRIPE_API_VERSION, MOCK_STRIPE_APP_INFO_URL, MOCK_STRIPE_SECRET_KEY, - MOCK_MAX_RETRY_COUNT, } from '../../../test/constants'; import { ServerConfigService } from '../../common/config/server-config.service'; import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { ControlledError } from '../../common/errors/controlled'; +import { EventType } from '../../common/enums/webhook'; +import { ValidationError } from '../../common/errors'; import { JobRepository } from '../job/job.repository'; - -jest.mock('@human-protocol/sdk'); +import { JobService } from '../job/job.service'; +import { Web3Service } from '../web3/web3.service'; +import { WebhookController } from './webhook.controller'; +import { WebhookDataDto } from './webhook.dto'; +import { WebhookRepository } from './webhook.repository'; +import { WebhookService } from './webhook.service'; describe('WebhookController', () => { let webhookController: WebhookController; @@ -163,17 +163,12 @@ describe('WebhookController', () => { jest .spyOn(jobService, 'escrowFailedWebhook') .mockImplementation(async () => { - throw new ControlledError( - 'Invalid manifest URL', - HttpStatus.BAD_REQUEST, - ); + throw new ValidationError('Invalid manifest URL'); }); await expect( webhookController.processWebhook(invalidDto), - ).rejects.toThrow( - new ControlledError('Invalid manifest URL', HttpStatus.BAD_REQUEST), - ); + ).rejects.toThrow(new ValidationError('Invalid manifest URL')); expect(jobService.escrowFailedWebhook).toHaveBeenCalledWith(invalidDto); }); diff --git a/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.spec.ts b/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.spec.ts index 5d70b8157d..5456323842 100644 --- a/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.spec.ts @@ -1,8 +1,18 @@ +jest.mock('@human-protocol/sdk', () => ({ + ...jest.requireActual('@human-protocol/sdk'), + EscrowClient: { + build: jest.fn(), + }, +})); + +import { faker } from '@faker-js/faker/.'; import { createMock } from '@golevelup/ts-jest'; import { ChainId, EscrowClient, KVStoreUtils } from '@human-protocol/sdk'; import { HttpService } from '@nestjs/axios'; +import { HttpStatus } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test } from '@nestjs/testing'; +import { of, throwError } from 'rxjs'; import { MOCK_ADDRESS, MOCK_EXCHANGE_ORACLE_ADDRESS, @@ -10,34 +20,24 @@ import { MOCK_MAX_RETRY_COUNT, mockConfig, } from '../../../test/constants'; +import { ServerConfigService } from '../../common/config/server-config.service'; +import { Web3ConfigService } from '../../common/config/web3-config.service'; +import { HEADER_SIGNATURE_KEY } from '../../common/constants'; import { ErrorWebhook } from '../../common/constants/errors'; +import { FortuneJobType } from '../../common/enums/job'; import { EventType, OracleType, WebhookStatus, } from '../../common/enums/webhook'; +import { ServerError, ValidationError } from '../../common/errors'; +import { JobRepository } from '../job/job.repository'; +import { JobService } from '../job/job.service'; import { Web3Service } from '../web3/web3.service'; +import { WebhookDataDto } from './webhook.dto'; import { WebhookEntity } from './webhook.entity'; import { WebhookRepository } from './webhook.repository'; import { WebhookService } from './webhook.service'; -import { of } from 'rxjs'; -import { HEADER_SIGNATURE_KEY } from '../../common/constants'; -import { JobService } from '../job/job.service'; -import { WebhookDataDto } from './webhook.dto'; -import { HttpStatus } from '@nestjs/common'; -import { ServerConfigService } from '../../common/config/server-config.service'; -import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { ControlledError } from '../../common/errors/controlled'; -import { JobRepository } from '../job/job.repository'; -import { FortuneJobType } from '../../common/enums/job'; -import { faker } from '@faker-js/faker/.'; - -jest.mock('@human-protocol/sdk', () => ({ - ...jest.requireActual('@human-protocol/sdk'), - EscrowClient: { - build: jest.fn(), - }, -})); describe('WebhookService', () => { let webhookService: WebhookService, @@ -122,9 +122,7 @@ describe('WebhookService', () => { .mockResolvedValue(''); await expect( (webhookService as any).sendWebhook(webhookEntity), - ).rejects.toThrow( - new ControlledError(ErrorWebhook.UrlNotFound, HttpStatus.NOT_FOUND), - ); + ).rejects.toThrow(new ServerError(ErrorWebhook.UrlNotFound)); }); it('should handle error if any exception is thrown', async () => { @@ -132,15 +130,11 @@ describe('WebhookService', () => { .spyOn(webhookService as any, 'getExchangeOracleWebhookUrl') .mockResolvedValue(MOCK_EXCHANGE_ORACLE_WEBHOOK_URL); jest.spyOn(httpService as any, 'post').mockImplementation(() => { - return of({ - data: undefined, - }); + return throwError(() => new Error('HTTP request failed')); }); await expect( (webhookService as any).sendWebhook(webhookEntity), - ).rejects.toThrow( - new ControlledError(ErrorWebhook.NotSent, HttpStatus.NOT_FOUND), - ); + ).rejects.toThrow(new ServerError('HTTP request failed')); }); it('should successfully process a fortune webhook', async () => { @@ -382,10 +376,7 @@ describe('WebhookService', () => { }; await expect(webhookService.handleWebhook(webhook)).rejects.toThrow( - new ControlledError( - 'Invalid webhook event type: escrow_canceled', - HttpStatus.BAD_REQUEST, - ), + new ValidationError(`Invalid webhook event type: ${webhook.eventType}`), ); }); }); diff --git a/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.ts b/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.ts index 7a6076ee20..4bb967ca39 100644 --- a/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.ts +++ b/packages/apps/job-launcher/server/src/modules/webhook/webhook.service.ts @@ -1,30 +1,33 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ChainId, EscrowClient, KVStoreKeys, KVStoreUtils, } from '@human-protocol/sdk'; +import { HttpService } from '@nestjs/axios'; +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { firstValueFrom } from 'rxjs'; import { ServerConfigService } from '../../common/config/server-config.service'; import { Web3ConfigService } from '../../common/config/web3-config.service'; -import { signMessage } from '../../common/utils/signature'; -import { WebhookRepository } from './webhook.repository'; -import { firstValueFrom } from 'rxjs'; import { HEADER_SIGNATURE_KEY } from '../../common/constants'; -import { HttpService } from '@nestjs/axios'; -import { Web3Service } from '../web3/web3.service'; -import { WebhookStatus } from '../../common/enums/webhook'; import { ErrorWebhook } from '../../common/constants/errors'; -import { WebhookEntity } from './webhook.entity'; -import { WebhookDataDto } from './webhook.dto'; +import { EventType, WebhookStatus } from '../../common/enums/webhook'; +import { ServerError, ValidationError } from '../../common/errors'; import { CaseConverter } from '../../common/utils/case-converter'; -import { EventType } from '../../common/enums/webhook'; -import { JobService } from '../job/job.service'; -import { ControlledError } from '../../common/errors/controlled'; +import { formatAxiosError } from '../../common/utils/http'; +import { signMessage } from '../../common/utils/signature'; import { JobRepository } from '../job/job.repository'; +import { JobService } from '../job/job.service'; +import { Web3Service } from '../web3/web3.service'; +import { WebhookDataDto } from './webhook.dto'; +import { WebhookEntity } from './webhook.entity'; +import { WebhookRepository } from './webhook.repository'; + @Injectable() export class WebhookService { + private readonly logger = new Logger(WebhookService.name); + constructor( @Inject(Web3Service) private readonly web3Service: Web3Service, @@ -52,7 +55,7 @@ export class WebhookService { // Check if the webhook URL was found. if (!webhookUrl) { - throw new ControlledError(ErrorWebhook.UrlNotFound, HttpStatus.NOT_FOUND); + throw new ServerError(ErrorWebhook.UrlNotFound); } // Build the webhook data object based on the oracle type. @@ -75,13 +78,16 @@ export class WebhookService { } // Make the HTTP request to the webhook. - const { status } = await firstValueFrom( - this.httpService.post(webhookUrl, webhookData, config), - ); - - // Check if the request was successful. - if (status !== HttpStatus.CREATED && status !== HttpStatus.OK) { - throw new ControlledError(ErrorWebhook.NotSent, HttpStatus.NOT_FOUND); + try { + await firstValueFrom( + this.httpService.post(webhookUrl, webhookData, config), + ); + } catch (error) { + const formattedError = formatAxiosError(error); + this.logger.error('Webhook not sent', { + error: formattedError, + }); + throw new Error(formattedError.message); } } @@ -144,9 +150,8 @@ export class WebhookService { break; default: - throw new ControlledError( + throw new ValidationError( `Invalid webhook event type: ${webhook.eventType}`, - HttpStatus.BAD_REQUEST, ); } } @@ -158,10 +163,7 @@ export class WebhookService { ); if (!jobEntity) { - throw new ControlledError( - ErrorWebhook.InvalidEscrow, - HttpStatus.BAD_REQUEST, - ); + throw new Error(ErrorWebhook.InvalidEscrow); } const webhookEntity = new WebhookEntity();