diff --git a/packages/core/e2e/error-handler-strategy.e2e-spec.ts b/packages/core/e2e/error-handler-strategy.e2e-spec.ts new file mode 100644 index 0000000000..1babc10be9 --- /dev/null +++ b/packages/core/e2e/error-handler-strategy.e2e-spec.ts @@ -0,0 +1,147 @@ +import { ArgumentsHost, OnApplicationBootstrap } from '@nestjs/common'; +import { Mutation, Resolver } from '@nestjs/graphql'; +import { + mergeConfig, + Job, + ErrorHandlerStrategy, + VendurePlugin, + PluginCommonModule, + JobQueueService, + JobQueue, +} from '@vendure/core'; +import { createTestEnvironment } from '@vendure/testing'; +import gql from 'graphql-tag'; +import path from 'path'; +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { initialData } from '../../../e2e-common/e2e-initial-data'; +import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; + +import { awaitRunningJobs } from './utils/await-running-jobs'; + +class TestErrorHandlerStrategy implements ErrorHandlerStrategy { + static serverErrorSpy = vi.fn(); + static workerErrorSpy = vi.fn(); + + handleServerError(exception: Error, context: { host: ArgumentsHost }) { + TestErrorHandlerStrategy.serverErrorSpy(exception, context); + } + + handleWorkerError(exception: Error, context: { job: Job }) { + TestErrorHandlerStrategy.workerErrorSpy(exception, context); + } +} + +@Resolver() +class TestResolver implements OnApplicationBootstrap { + private queue: JobQueue; + constructor(private jobQueueService: JobQueueService) {} + + async onApplicationBootstrap() { + this.queue = await this.jobQueueService.createQueue({ + name: 'test-queue', + process: async () => { + throw new Error('worker error'); + }, + }); + } + + @Mutation() + createServerError() { + throw new Error('server error'); + } + + @Mutation() + createWorkerError() { + return this.queue.add({}); + } +} + +@VendurePlugin({ + imports: [PluginCommonModule], + adminApiExtensions: { + schema: gql` + extend type Mutation { + createServerError: Boolean! + createWorkerError: Job! + } + `, + resolvers: [TestResolver], + }, +}) +class TestErrorMakingPlugin {} + +describe('ErrorHandlerStrategy', () => { + const { server, adminClient, shopClient } = createTestEnvironment( + mergeConfig(testConfig(), { + systemOptions: { + errorHandlers: [new TestErrorHandlerStrategy()], + }, + plugins: [TestErrorMakingPlugin], + }), + ); + + beforeAll(async () => { + await server.init({ + initialData, + productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'), + customerCount: 1, + }); + await adminClient.asSuperAdmin(); + }, TEST_SETUP_TIMEOUT_MS); + + beforeEach(() => { + TestErrorHandlerStrategy.serverErrorSpy.mockClear(); + TestErrorHandlerStrategy.workerErrorSpy.mockClear(); + }); + + afterAll(async () => { + await server.destroy(); + }); + + it('no error handlers have initially been called', async () => { + expect(TestErrorHandlerStrategy.serverErrorSpy).toHaveBeenCalledTimes(0); + expect(TestErrorHandlerStrategy.workerErrorSpy).toHaveBeenCalledTimes(0); + }); + + it('invokes the server handler', async () => { + try { + await adminClient.query( + gql` + mutation { + createServerError + } + `, + ); + } catch (e: any) { + expect(e.message).toBe('server error'); + } + expect(TestErrorHandlerStrategy.serverErrorSpy).toHaveBeenCalledTimes(1); + expect(TestErrorHandlerStrategy.workerErrorSpy).toHaveBeenCalledTimes(0); + + expect(TestErrorHandlerStrategy.serverErrorSpy.mock.calls[0][0]).toBeInstanceOf(Error); + expect(TestErrorHandlerStrategy.serverErrorSpy.mock.calls[0][0].message).toBe('server error'); + expect(TestErrorHandlerStrategy.serverErrorSpy.mock.calls[0][1].host).toBeDefined(); + }); + + it('invokes the worker handler', async () => { + await adminClient.query( + gql` + mutation { + createWorkerError { + id + } + } + `, + ); + await awaitRunningJobs(adminClient); + expect(TestErrorHandlerStrategy.serverErrorSpy).toHaveBeenCalledTimes(0); + expect(TestErrorHandlerStrategy.workerErrorSpy).toHaveBeenCalledTimes(1); + + expect(TestErrorHandlerStrategy.workerErrorSpy.mock.calls[0][0]).toBeInstanceOf(Error); + expect(TestErrorHandlerStrategy.workerErrorSpy.mock.calls[0][0].message).toBe('worker error'); + expect(TestErrorHandlerStrategy.workerErrorSpy.mock.calls[0][1].job).toContain({ + queueName: 'test-queue', + }); + }); +}); diff --git a/packages/core/src/config/system/error-handler-strategy.ts b/packages/core/src/config/system/error-handler-strategy.ts index cafd40a6f0..1f7f74f730 100644 --- a/packages/core/src/config/system/error-handler-strategy.ts +++ b/packages/core/src/config/system/error-handler-strategy.ts @@ -1,7 +1,7 @@ import { ArgumentsHost } from '@nestjs/common'; import { InjectableStrategy } from '../../common/index'; -import { Job } from '../../job-queue/index'; +import { Job } from '../../job-queue/job'; /** * @description