From b122847194e1dbc8cf17961e672c138a4770e33e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:43:58 +0200 Subject: [PATCH] Fix exposure of internal error messages in import endpoint --- .../import/import.controller.spec.ts | 57 +++++++++++++++++++ .../app/endpoints/import/import.controller.ts | 7 ++- 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/app/endpoints/import/import.controller.spec.ts diff --git a/apps/api/src/app/endpoints/import/import.controller.spec.ts b/apps/api/src/app/endpoints/import/import.controller.spec.ts new file mode 100644 index 0000000000..4a6971fde5 --- /dev/null +++ b/apps/api/src/app/endpoints/import/import.controller.spec.ts @@ -0,0 +1,57 @@ +import { ImportValidationError } from '@ghostfolio/api/services/import/errors/import-validation.error'; +import { ImportDataDto } from '@ghostfolio/api/services/import/import-data.dto'; +import { ImportService } from '@ghostfolio/api/services/import/import.service'; +import { permissions } from '@ghostfolio/common/permissions'; +import type { RequestWithUser } from '@ghostfolio/common/types'; + +import { Logger } from '@nestjs/common'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; + +import { ImportController } from './import.controller'; + +describe('ImportController', () => { + const importData = { activities: [] } as ImportDataDto; + + let importController: ImportController; + let importService: ImportService; + + beforeEach(() => { + jest.spyOn(Logger.prototype, 'error').mockImplementation(); + + importService = { + import: jest.fn() + } as unknown as ImportService; + + importController = new ImportController(importService, { + user: { permissions: [permissions.createAccount] } + } as unknown as RequestWithUser); + }); + + describe('import', () => { + it('Gives the status 400 with the message of an error for the caller', async () => { + jest + .mocked(importService.import) + .mockRejectedValueOnce( + new ImportValidationError('activities.0.accountId ("1") is not valid') + ); + + await expect(importController.import(importData)).rejects.toMatchObject({ + response: { message: ['activities.0.accountId ("1") is not valid'] }, + status: StatusCodes.BAD_REQUEST + }); + }); + + it('Gives the status 400 without the message of every other error', async () => { + jest + .mocked(importService.import) + .mockRejectedValueOnce( + new Error('Invalid `prisma.order.create()` invocation') + ); + + await expect(importController.import(importData)).rejects.toMatchObject({ + response: { message: [getReasonPhrase(StatusCodes.BAD_REQUEST)] }, + status: StatusCodes.BAD_REQUEST + }); + }); + }); +}); diff --git a/apps/api/src/app/endpoints/import/import.controller.ts b/apps/api/src/app/endpoints/import/import.controller.ts index 846914bd03..57093f8ee0 100644 --- a/apps/api/src/app/endpoints/import/import.controller.ts +++ b/apps/api/src/app/endpoints/import/import.controller.ts @@ -1,4 +1,5 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; +import { CallerFacingError } from '@ghostfolio/api/errors/caller-facing.error'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; @@ -73,7 +74,11 @@ export class ImportController { throw new HttpException( { error: getReasonPhrase(StatusCodes.BAD_REQUEST), - message: [error.message] + message: [ + error instanceof CallerFacingError + ? error.message + : getReasonPhrase(StatusCodes.BAD_REQUEST) + ] }, StatusCodes.BAD_REQUEST );