diff --git a/packages/@n8n/db/jest.config.js b/packages/@n8n/db/jest.config.js deleted file mode 100644 index 7d610f919f4..00000000000 --- a/packages/@n8n/db/jest.config.js +++ /dev/null @@ -1,10 +0,0 @@ -const baseConfig = require('../../../jest.config'); - -/** @type {import('jest').Config} */ -module.exports = { - ...baseConfig, - transform: { - ...baseConfig.transform, - '^.+\\.ts$': ['ts-jest', { isolatedModules: false }], - }, -}; diff --git a/packages/@n8n/db/package.json b/packages/@n8n/db/package.json index be3c525819f..555b0c64b2f 100644 --- a/packages/@n8n/db/package.json +++ b/packages/@n8n/db/package.json @@ -12,9 +12,9 @@ "lint": "eslint . --quiet", "lint:fix": "eslint . --fix", "watch": "tsc -p tsconfig.build.json --watch", - "test": "jest", - "test:unit": "jest", - "test:dev": "jest --watch", + "test": "vitest run", + "test:unit": "vitest run", + "test:dev": "vitest --silent=false", "migration:new": "node scripts/new-migration.mjs" }, "main": "dist/index.js", @@ -47,7 +47,12 @@ }, "devDependencies": { "@n8n/typescript-config": "workspace:*", + "@n8n/vitest-config": "workspace:*", "@types/lodash": "catalog:", - "express": "5.1.0" + "@vitest/coverage-v8": "catalog:", + "express": "5.1.0", + "typescript": "catalog:", + "vitest": "catalog:", + "vitest-mock-extended": "catalog:" } } diff --git a/packages/@n8n/db/src/connection/__tests__/db-connection-monitor.test.ts b/packages/@n8n/db/src/connection/__tests__/db-connection-monitor.test.ts index a1b67241720..61829b4cf4b 100644 --- a/packages/@n8n/db/src/connection/__tests__/db-connection-monitor.test.ts +++ b/packages/@n8n/db/src/connection/__tests__/db-connection-monitor.test.ts @@ -2,38 +2,39 @@ import type { Logger } from '@n8n/backend-common'; import type { DatabaseConfig } from '@n8n/config'; import type { DataSource } from '@n8n/typeorm'; -import { mock, mockDeep } from 'jest-mock-extended'; import type { ErrorReporter } from 'n8n-core'; import type TimersPromises from 'timers/promises'; import { setTimeout as setTimeoutP } from 'timers/promises'; +import type { Mock, MockedFunction } from 'vitest'; +import { mock, mockDeep } from 'vitest-mock-extended'; import { DbConnectionMonitor } from '../db-connection-monitor'; // The monitor uses `setTimeout` from `timers/promises` for recovery backoff. // Mocking it lets us drive the recovery loop deterministically without juggling -// jest fake timers against async/await microtask ordering. -jest.mock('timers/promises', () => { - const actual = jest.requireActual('timers/promises'); - return { ...actual, setTimeout: jest.fn() }; +// fake timers against async/await microtask ordering. +vi.mock('timers/promises', async () => { + const actual = await vi.importActual('timers/promises'); + return { ...actual, setTimeout: vi.fn() }; }); -const mockedSetTimeoutP = setTimeoutP as jest.MockedFunction; +const mockedSetTimeoutP = setTimeoutP as MockedFunction; const flushMicrotasks = async () => await new Promise((resolve) => setImmediate(resolve)); describe('DbConnectionMonitor', () => { let monitor: DbConnectionMonitor; - let onConnectedChange: jest.MockedFunction<(connected: boolean) => void>; + let onConnectedChange: MockedFunction<(connected: boolean) => void>; const errorReporter = mock(); const databaseConfig = mock({ pingTimeoutMs: 5_000 }); const logger = mock(); const dataSource = mockDeep({ options: { type: 'postgres' } }); beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); // Default: never resolves, so query wins the ping timeout race and // recovery backoff stays suspended unless a test overrides it. mockedSetTimeoutP.mockImplementation(async () => await new Promise(() => {})); - onConnectedChange = jest.fn(); + onConnectedChange = vi.fn(); monitor = new DbConnectionMonitor( dataSource, onConnectedChange, @@ -110,7 +111,7 @@ describe('DbConnectionMonitor', () => { // eslint-disable-next-line @typescript-eslint/naming-convention dataSource.query.mockResolvedValue([{ '1': 1 }]); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const scheduleNextPingSpy = jest.spyOn(monitor as any, 'scheduleNextPing'); + const scheduleNextPingSpy = vi.spyOn(monitor as any, 'scheduleNextPing'); // @ts-expect-error private property await monitor.ping(); @@ -150,7 +151,7 @@ describe('DbConnectionMonitor', () => { .mockRejectedValueOnce( new Error('Client has encountered a connection error and is not queryable'), ); - const recoverSpy = jest + const recoverSpy = vi // eslint-disable-next-line @typescript-eslint/no-explicit-any .spyOn(monitor as any, 'recoverDataSource') .mockResolvedValue(undefined); @@ -171,7 +172,7 @@ describe('DbConnectionMonitor', () => { // @ts-expect-error readonly property dataSource.isInitialized = true; dataSource.query.mockRejectedValue(new Error('pool poisoned')); - const recoverSpy = jest + const recoverSpy = vi // eslint-disable-next-line @typescript-eslint/no-explicit-any .spyOn(monitor as any, 'recoverDataSource') .mockResolvedValue(undefined); @@ -234,7 +235,7 @@ describe('DbConnectionMonitor', () => { }); it('should execute ping on schedule', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); try { const scheduledMonitor = new DbConnectionMonitor( dataSource, @@ -244,20 +245,20 @@ describe('DbConnectionMonitor', () => { errorReporter, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pingSpy = jest.spyOn(scheduledMonitor as any, 'ping'); + const pingSpy = vi.spyOn(scheduledMonitor as any, 'ping'); // @ts-expect-error private property scheduledMonitor.scheduleNextPing(); - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); expect(pingSpy).toHaveBeenCalled(); } finally { - jest.useRealTimers(); + vi.useRealTimers(); } }); it('should not schedule another ping after stop', () => { - jest.useFakeTimers(); + vi.useFakeTimers(); try { const scheduledMonitor = new DbConnectionMonitor( dataSource, @@ -267,16 +268,16 @@ describe('DbConnectionMonitor', () => { errorReporter, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const pingSpy = jest.spyOn(scheduledMonitor as any, 'ping'); + const pingSpy = vi.spyOn(scheduledMonitor as any, 'ping'); scheduledMonitor.stop(); // @ts-expect-error private property scheduledMonitor.scheduleNextPing(); - jest.advanceTimersByTime(1000); + vi.advanceTimersByTime(1000); expect(pingSpy).not.toHaveBeenCalled(); } finally { - jest.useRealTimers(); + vi.useRealTimers(); } }); }); @@ -487,8 +488,8 @@ describe('DbConnectionMonitor', () => { dataSource.isInitialized = true; dataSource.destroy.mockResolvedValue(); dataSource.initialize.mockResolvedValue(dataSource); - const on = jest.fn(); - (dataSource as unknown as { driver: { master: { on: jest.Mock } } }).driver = { + const on = vi.fn(); + (dataSource as unknown as { driver: { master: { on: Mock } } }).driver = { master: { on }, }; @@ -531,7 +532,7 @@ describe('DbConnectionMonitor', () => { }; it('should attach an error listener to the Postgres driver pool', () => { - const on = jest.fn(); + const on = vi.fn(); setDriver({ master: { on } }); monitor.start(); @@ -541,7 +542,7 @@ describe('DbConnectionMonitor', () => { it('should mark the connection unhealthy when the pool emits an error', () => { let handler: ((cause: unknown) => void) | undefined; - const on = jest.fn((_event: string, h: (cause: unknown) => void) => { + const on = vi.fn((_event: string, h: (cause: unknown) => void) => { handler = h; }); setDriver({ master: { on } }); @@ -590,7 +591,7 @@ describe('DbConnectionMonitor', () => { describe('stop', () => { it('should clear the ping timer', () => { - const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout'); + const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout'); // @ts-expect-error private property monitor.pingTimer = setTimeout(() => {}, 1000); @@ -615,7 +616,7 @@ describe('DbConnectionMonitor', () => { // initial state is "connected". If the default flipped to false, the first failed // ping would be a no-op transition (false → false) and the owner's state machine // would stay stuck at the manually-set `true` while reality is `false`. - const freshOnConnectedChange = jest.fn(); + const freshOnConnectedChange = vi.fn(); const freshMonitor = new DbConnectionMonitor( dataSource, freshOnConnectedChange, diff --git a/packages/@n8n/db/src/connection/__tests__/db-connection-options.test.ts b/packages/@n8n/db/src/connection/__tests__/db-connection-options.test.ts index 4db9bf35131..2990bedee9a 100644 --- a/packages/@n8n/db/src/connection/__tests__/db-connection-options.test.ts +++ b/packages/@n8n/db/src/connection/__tests__/db-connection-options.test.ts @@ -1,7 +1,7 @@ import type { ModuleRegistry } from '@n8n/backend-common'; import type { GlobalConfig, InstanceSettingsConfig } from '@n8n/config'; -import { mock } from 'jest-mock-extended'; import path from 'path'; +import { mock } from 'vitest-mock-extended'; import { postgresMigrations } from '../../migrations/postgresdb'; import { sqliteMigrations } from '../../migrations/sqlite'; @@ -24,7 +24,7 @@ describe('DbConnectionOptions', () => { moduleRegistry, ); - beforeEach(() => jest.resetAllMocks()); + beforeEach(() => vi.resetAllMocks()); const commonOptions = { entityPrefix: 'test_prefix_', diff --git a/packages/@n8n/db/src/connection/__tests__/db-connection.test.ts b/packages/@n8n/db/src/connection/__tests__/db-connection.test.ts index 10549e77651..695ec200655 100644 --- a/packages/@n8n/db/src/connection/__tests__/db-connection.test.ts +++ b/packages/@n8n/db/src/connection/__tests__/db-connection.test.ts @@ -2,9 +2,10 @@ import type { Logger } from '@n8n/backend-common'; import type { DatabaseConfig } from '@n8n/config'; import { DataSource, type DataSourceOptions } from '@n8n/typeorm'; -import { mock, mockDeep } from 'jest-mock-extended'; import type { ErrorReporter } from 'n8n-core'; import { DbConnectionTimeoutError } from 'n8n-workflow'; +import type { Mock } from 'vitest'; +import { mock, mockDeep } from 'vitest-mock-extended'; import * as migrationHelper from '../../migrations/migration-helpers'; import type { Migration } from '../../migrations/migration-types'; @@ -12,14 +13,14 @@ import { DbConnection } from '../db-connection'; import { DbConnectionMonitor } from '../db-connection-monitor'; import type { DbConnectionOptions } from '../db-connection-options'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-return -jest.mock('@n8n/typeorm', () => ({ +vi.mock('@n8n/typeorm', async () => ({ + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + ...(await vi.importActual('@n8n/typeorm')), // eslint-disable-next-line @typescript-eslint/naming-convention - DataSource: jest.fn(), - ...jest.requireActual('@n8n/typeorm'), + DataSource: vi.fn(), })); -jest.mock('../db-connection-monitor'); +vi.mock('../db-connection-monitor'); describe('DbConnection', () => { let dbConnection: DbConnection; @@ -42,11 +43,15 @@ describe('DbConnection', () => { const monitor = mock(); beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); connectionOptions.getOptions.mockReturnValue(postgresOptions); - (DataSource as jest.Mock) = jest.fn().mockImplementation(() => dataSource); - jest.mocked(DbConnectionMonitor).mockImplementation(() => monitor); + vi.mocked(DbConnectionMonitor).mockImplementation(function () { + return monitor; + }); + (DataSource as unknown as Mock) = vi.fn(function () { + return dataSource; + }); dbConnection = new DbConnection(errorReporter, connectionOptions, databaseConfig, logger); }); @@ -101,7 +106,9 @@ describe('DbConnection', () => { it('should wrap migrations and run them', async () => { dataSource.runMigrations.mockResolvedValue([]); - const wrapMigrationSpy = jest.spyOn(migrationHelper, 'wrapMigration').mockImplementation(); + const wrapMigrationSpy = vi + .spyOn(migrationHelper, 'wrapMigration') + .mockImplementation(() => {}); expect(dataSource.runMigrations).not.toHaveBeenCalled(); expect(dbConnection.connectionState.migrated).toBe(false); diff --git a/packages/@n8n/db/src/migrations/__tests__/migration-helpers.ts b/packages/@n8n/db/src/migrations/__tests__/migration-helpers.ts index 5109fa3cf3c..fe85438bc97 100644 --- a/packages/@n8n/db/src/migrations/__tests__/migration-helpers.ts +++ b/packages/@n8n/db/src/migrations/__tests__/migration-helpers.ts @@ -23,7 +23,7 @@ describe('migrationHelpers.wrapMigration', () => { class TestMigration implements IrreversibleMigration { async up() {} } - const originalUp = jest.fn(); + const originalUp = vi.fn(); TestMigration.prototype.up = originalUp; // @@ -48,7 +48,7 @@ describe('migrationHelpers.wrapMigration', () => { async down() {} } - const originalDown = jest.fn(); + const originalDown = vi.fn(); TestMigration.prototype.down = originalDown; // diff --git a/packages/@n8n/db/src/migrations/dsl/__tests__/enum-check.test.ts b/packages/@n8n/db/src/migrations/dsl/__tests__/enum-check.test.ts index 6f3bbd4ff05..e451a4588dc 100644 --- a/packages/@n8n/db/src/migrations/dsl/__tests__/enum-check.test.ts +++ b/packages/@n8n/db/src/migrations/dsl/__tests__/enum-check.test.ts @@ -1,5 +1,5 @@ import type { Driver, QueryRunner, Table } from '@n8n/typeorm'; -import { mock } from 'jest-mock-extended'; +import { mock } from 'vitest-mock-extended'; import { Column } from '../column'; import { AddColumns, AddEnumCheck, CreateTable, DropEnumCheck } from '../table'; diff --git a/packages/@n8n/db/src/migrations/migration-helpers.test.ts b/packages/@n8n/db/src/migrations/migration-helpers.test.ts index bb33ecf0492..bb9aead1287 100644 --- a/packages/@n8n/db/src/migrations/migration-helpers.test.ts +++ b/packages/@n8n/db/src/migrations/migration-helpers.test.ts @@ -57,7 +57,7 @@ describe('Migration Helpers', () => { const queryRunner = dataSource.createQueryRunner(); // Spy on queryRunner.query to capture the actual SQL being executed - const querySpy = jest.spyOn(queryRunner, 'query'); + const querySpy = vi.spyOn(queryRunner, 'query'); // Copy all data using copyTable (should trigger multiple batches with OFFSET) await copyTable(queryRunner, '', testTableName, destTableName); diff --git a/packages/@n8n/db/src/repositories/__tests__/credential-dependency.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/credential-dependency.repository.test.ts index 06656bfd04c..0e5f065a203 100644 --- a/packages/@n8n/db/src/repositories/__tests__/credential-dependency.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/credential-dependency.repository.test.ts @@ -13,7 +13,7 @@ describe('CredentialDependencyRepository', () => { const repository = Container.get(CredentialDependencyRepository); beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); describe('findCredentialIdsByDependencyId', () => { @@ -39,11 +39,11 @@ describe('CredentialDependencyRepository', () => { describe('upsertDependenciesForCredential', () => { it('deduplicates ids and inserts once with orIgnore', async () => { const qb = { - insert: jest.fn().mockReturnThis(), - into: jest.fn().mockReturnThis(), - values: jest.fn().mockReturnThis(), - orIgnore: jest.fn().mockReturnThis(), - execute: jest.fn().mockResolvedValue(undefined), + insert: vi.fn().mockReturnThis(), + into: vi.fn().mockReturnThis(), + values: vi.fn().mockReturnThis(), + orIgnore: vi.fn().mockReturnThis(), + execute: vi.fn().mockResolvedValue(undefined), }; entityManager.createQueryBuilder.mockReturnValue(qb as never); @@ -89,7 +89,7 @@ describe('CredentialDependencyRepository', () => { { dependencyId: 'provider-keep' } as CredentialDependency, ]); - const upsertSpy = jest + const upsertSpy = vi .spyOn(repository, 'upsertDependenciesForCredential') .mockResolvedValue(undefined); @@ -158,7 +158,7 @@ describe('CredentialDependencyRepository', () => { describe('addCredentialDependencyExistsFilter', () => { it('applies the EXISTS dependency filter using andWhere', () => { const qb = { - andWhere: jest.fn().mockReturnThis(), + andWhere: vi.fn().mockReturnThis(), }; const filter = { dependencyType: 'externalSecretProvider', diff --git a/packages/@n8n/db/src/repositories/__tests__/credentials.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/credentials.repository.test.ts index 4729050d06b..223dd53f1b5 100644 --- a/packages/@n8n/db/src/repositories/__tests__/credentials.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/credentials.repository.test.ts @@ -1,7 +1,7 @@ import { Container } from '@n8n/di'; import type { SelectQueryBuilder } from '@n8n/typeorm'; import { In } from '@n8n/typeorm'; -import { mock } from 'jest-mock-extended'; +import { mock } from 'vitest-mock-extended'; import { CredentialsEntity } from '../../entities'; import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager'; @@ -12,7 +12,7 @@ describe('CredentialsRepository', () => { const credentialsRepository = Container.get(CredentialsRepository); beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); describe('findManyAndCount', () => { @@ -58,13 +58,13 @@ describe('CredentialsRepository', () => { describe('findAllGlobalCredentials', () => { it('applies dependency filter through query builder when provided', async () => { - const andWhereSpy = jest.fn().mockReturnThis(); - const getManySpy = jest.fn().mockResolvedValue([]); + const andWhereSpy = vi.fn().mockReturnThis(); + const getManySpy = vi.fn().mockResolvedValue([]); const qb = mock>({ andWhere: andWhereSpy, getMany: getManySpy, }); - jest.spyOn(credentialsRepository, 'createQueryBuilder').mockReturnValue(qb); + vi.spyOn(credentialsRepository, 'createQueryBuilder').mockReturnValue(qb); await credentialsRepository.findAllGlobalCredentials({ filters: { diff --git a/packages/@n8n/db/src/repositories/__tests__/evaluation-collection.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/evaluation-collection.repository.test.ts index 831300a9a2b..7fd4fcb7e97 100644 --- a/packages/@n8n/db/src/repositories/__tests__/evaluation-collection.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/evaluation-collection.repository.test.ts @@ -1,4 +1,5 @@ import { Container } from '@n8n/di'; +import type { Mock } from 'vitest'; import { EvaluationCollection } from '../../entities/evaluation-collection.ee'; import { TestRun } from '../../entities/test-run.ee'; @@ -10,12 +11,12 @@ describe('EvaluationCollectionRepository', () => { const repo = Container.get(EvaluationCollectionRepository); beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); describe('createCollection', () => { it('persists the collection with insightsCache initialised to null', async () => { - (entityManager.create as jest.Mock).mockImplementation( + (entityManager.create as Mock).mockImplementation( (_target: unknown, entityLike: unknown) => entityLike as EvaluationCollection, ); entityManager.save.mockImplementationOnce(async (_target, entity) => entity); @@ -62,11 +63,11 @@ describe('EvaluationCollectionRepository', () => { ] as EvaluationCollection[]; entityManager.find.mockResolvedValueOnce(collections); const qb = { - select: jest.fn().mockReturnThis(), - addSelect: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - groupBy: jest.fn().mockReturnThis(), - getRawMany: jest.fn().mockResolvedValueOnce([ + select: vi.fn().mockReturnThis(), + addSelect: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + groupBy: vi.fn().mockReturnThis(), + getRawMany: vi.fn().mockResolvedValueOnce([ { collectionId: 'col-a', count: '3' }, { collectionId: 'col-b', count: '0' }, ]), @@ -185,7 +186,7 @@ describe('EvaluationCollectionRepository', () => { workflowId: 'wf-1', } as EvaluationCollection; entityManager.findOne.mockResolvedValueOnce(existing); - (entityManager.create as jest.Mock).mockImplementation( + (entityManager.create as Mock).mockImplementation( (_target: unknown, entityLike: unknown) => entityLike as EvaluationCollection, ); entityManager.save.mockImplementationOnce(async (_target, entity) => entity); diff --git a/packages/@n8n/db/src/repositories/__tests__/evaluation-config.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/evaluation-config.repository.test.ts index 7a5bb39e4b7..804821a94f1 100644 --- a/packages/@n8n/db/src/repositories/__tests__/evaluation-config.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/evaluation-config.repository.test.ts @@ -1,5 +1,6 @@ import type { UpsertEvaluationConfigDto } from '@n8n/api-types'; import { Container } from '@n8n/di'; +import type { Mock } from 'vitest'; import { EvaluationConfig } from '../../entities/evaluation-config.ee'; import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager'; @@ -33,7 +34,7 @@ describe('EvaluationConfigRepository', () => { }) as UpsertEvaluationConfigDto; beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); describe('listByWorkflowId', () => { @@ -78,7 +79,7 @@ describe('EvaluationConfigRepository', () => { describe('createForWorkflow', () => { it('persists a new config with the supplied id and workflow id', async () => { const payload = buildPayload(); - (entityManager.create as jest.Mock).mockImplementation( + (entityManager.create as Mock).mockImplementation( (_target: unknown, entityLike: unknown) => entityLike as EvaluationConfig, ); entityManager.save.mockImplementationOnce(async (_target, entity) => entity); @@ -169,13 +170,13 @@ describe('EvaluationConfigRepository', () => { describe('countDistinctWorkflowsWithConfigs', () => { it('returns the count of distinct workflowIds that have at least one config', async () => { const qbMock = { - select: jest.fn().mockReturnThis(), - distinct: jest.fn().mockReturnThis(), - getCount: jest.fn().mockResolvedValueOnce(7), + select: vi.fn().mockReturnThis(), + distinct: vi.fn().mockReturnThis(), + getCount: vi.fn().mockResolvedValueOnce(7), }; - jest - .spyOn(repo, 'createQueryBuilder') - .mockReturnValueOnce(qbMock as unknown as ReturnType); + vi.spyOn(repo, 'createQueryBuilder').mockReturnValueOnce( + qbMock as unknown as ReturnType, + ); expect(await repo.countDistinctWorkflowsWithConfigs()).toBe(7); expect(qbMock.select).toHaveBeenCalledWith('evaluation_config.workflowId'); diff --git a/packages/@n8n/db/src/repositories/__tests__/execution.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/execution.repository.test.ts index b9ea7f9572b..9574edd205f 100644 --- a/packages/@n8n/db/src/repositories/__tests__/execution.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/execution.repository.test.ts @@ -4,10 +4,10 @@ import type { SqliteConfig } from '@n8n/config'; import { Container } from '@n8n/di'; import type { SelectQueryBuilder } from '@n8n/typeorm'; import { In, LessThan, LessThanOrEqual, And, Not } from '@n8n/typeorm'; -import { mock } from 'jest-mock-extended'; import { BinaryDataService } from 'n8n-core'; import type { IRunExecutionData, IWorkflowBase } from 'n8n-workflow'; import { nanoid } from 'nanoid'; +import { mock } from 'vitest-mock-extended'; import { ExecutionEntity } from '../../entities'; import type { IExecutionResponse } from '../../entities/types-db'; @@ -29,7 +29,7 @@ describe('ExecutionRepository', () => { const executionRepository = Container.get(ExecutionRepository); beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); describe('getExecutionsForPublicApi', () => { @@ -403,7 +403,7 @@ describe('ExecutionRepository', () => { }, } as unknown; - const updateSpy = jest.spyOn(executionRepository, 'updateExistingExecution'); + const updateSpy = vi.spyOn(executionRepository, 'updateExistingExecution'); const result = await executionRepository.stopDuringRun(mockExecution as IExecutionResponse); @@ -511,8 +511,8 @@ describe('ExecutionRepository', () => { describe('getWaitingExecutions', () => { const mockDate = new Date('2023-12-28 12:34:56.789Z'); - beforeAll(() => jest.useFakeTimers().setSystemTime(mockDate)); - afterAll(() => jest.useRealTimers()); + beforeAll(() => vi.useFakeTimers().setSystemTime(mockDate)); + afterAll(() => vi.useRealTimers()); test.each(['sqlite', 'postgresdb'] as const)( 'on %s, should only return executions with status=waiting', @@ -543,11 +543,11 @@ describe('ExecutionRepository', () => { const workflowId = nanoid(); const binaryDataService = Container.get(BinaryDataService); - jest.spyOn(executionRepository, 'createQueryBuilder').mockReturnValue( + vi.spyOn(executionRepository, 'createQueryBuilder').mockReturnValue( mock>({ - select: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([{ id: '1', workflowId }]), + select: vi.fn().mockReturnThis(), + andWhere: vi.fn().mockReturnThis(), + getMany: vi.fn().mockResolvedValue([{ id: '1', workflowId }]), }), ); @@ -578,7 +578,7 @@ describe('ExecutionRepository', () => { status: 'success', }); - const txCallback = jest.fn(); + const txCallback = vi.fn(); entityManager.transaction.mockImplementation(async (fn: unknown) => { await (fn as (em: typeof entityManager) => Promise)(entityManager); txCallback(); diff --git a/packages/@n8n/db/src/repositories/__tests__/secrets-provider-connection.repository.ee.test.ts b/packages/@n8n/db/src/repositories/__tests__/secrets-provider-connection.repository.ee.test.ts index ef6a149cdd8..7d8a1fb0fe0 100644 --- a/packages/@n8n/db/src/repositories/__tests__/secrets-provider-connection.repository.ee.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/secrets-provider-connection.repository.ee.test.ts @@ -1,6 +1,6 @@ import { Container } from '@n8n/di'; -import { mock } from 'jest-mock-extended'; import random from 'lodash/random'; +import { mock } from 'vitest-mock-extended'; import { SecretsProviderConnection } from '../../entities'; import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager'; @@ -24,7 +24,7 @@ describe('SecretsProviderConnectionRepository', () => { }; beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); describe('findAll', () => { diff --git a/packages/@n8n/db/src/repositories/__tests__/shared-credentials.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/shared-credentials.repository.test.ts index 49d821538f3..eef578f0cdc 100644 --- a/packages/@n8n/db/src/repositories/__tests__/shared-credentials.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/shared-credentials.repository.test.ts @@ -1,6 +1,7 @@ import { Container } from '@n8n/di'; import { In, type SelectQueryBuilder } from '@n8n/typeorm'; -import { mock } from 'jest-mock-extended'; +import type { Mocked } from 'vitest'; +import { mock } from 'vitest-mock-extended'; import { SharedCredentials } from '../../entities'; import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager'; @@ -10,10 +11,10 @@ describe('SharedCredentialsRepository', () => { const entityManager = mockEntityManager(SharedCredentials); const sharedCredentialsRepository = Container.get(SharedCredentialsRepository); - let queryBuilder: jest.Mocked>; + let queryBuilder: Mocked>; beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); queryBuilder = mock>(); queryBuilder.where.mockReturnThis(); @@ -21,7 +22,7 @@ describe('SharedCredentialsRepository', () => { queryBuilder.innerJoin.mockReturnThis(); queryBuilder.select.mockReturnThis(); - jest.spyOn(sharedCredentialsRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); + vi.spyOn(sharedCredentialsRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); }); describe('findByCredentialIds', () => { diff --git a/packages/@n8n/db/src/repositories/__tests__/shared-workflow.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/shared-workflow.repository.test.ts index d9362bf184d..80324056ad8 100644 --- a/packages/@n8n/db/src/repositories/__tests__/shared-workflow.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/shared-workflow.repository.test.ts @@ -1,6 +1,7 @@ import { Container } from '@n8n/di'; import type { SelectQueryBuilder } from '@n8n/typeorm'; -import { mock } from 'jest-mock-extended'; +import type { Mocked } from 'vitest'; +import { mock } from 'vitest-mock-extended'; import { SharedWorkflow } from '../../entities'; import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager'; @@ -10,10 +11,10 @@ describe('SharedWorkflowRepository', () => { mockEntityManager(SharedWorkflow); const sharedWorkflowRepository = Container.get(SharedWorkflowRepository); - let queryBuilder: jest.Mocked>; + let queryBuilder: Mocked>; beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); queryBuilder = mock>(); queryBuilder.where.mockReturnThis(); @@ -21,7 +22,7 @@ describe('SharedWorkflowRepository', () => { queryBuilder.innerJoin.mockReturnThis(); queryBuilder.select.mockReturnThis(); - jest.spyOn(sharedWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); + vi.spyOn(sharedWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); }); describe('getSharedPersonalWorkflowsCount', () => { diff --git a/packages/@n8n/db/src/repositories/__tests__/workflow.repository.test.ts b/packages/@n8n/db/src/repositories/__tests__/workflow.repository.test.ts index 0c85e6623f5..0617ed842c1 100644 --- a/packages/@n8n/db/src/repositories/__tests__/workflow.repository.test.ts +++ b/packages/@n8n/db/src/repositories/__tests__/workflow.repository.test.ts @@ -1,6 +1,7 @@ import { GlobalConfig } from '@n8n/config'; import { In, type SelectQueryBuilder } from '@n8n/typeorm'; -import { mock } from 'jest-mock-extended'; +import type { Mock, Mocked } from 'vitest'; +import { mock } from 'vitest-mock-extended'; import { WorkflowEntity } from '../../entities'; import { mockEntityManager } from '../../utils/test-utils/mock-entity-manager'; @@ -26,10 +27,10 @@ describe('WorkflowRepository', () => { workflowHistoryRepository, ); - let queryBuilder: jest.Mocked>; + let queryBuilder: Mocked>; beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); queryBuilder = mock>(); @@ -54,7 +55,7 @@ describe('WorkflowRepository', () => { writable: true, }); - jest.spyOn(workflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); + vi.spyOn(workflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); }); describe('applyNameFilter', () => { @@ -118,7 +119,7 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); // andWhere should not be called for name filter - const nameFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) => + const nameFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) => call[0]?.includes('workflow.name'), ); expect(nameFilterCalls).toHaveLength(0); @@ -133,7 +134,7 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); // andWhere should not be called for name filter - const nameFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) => + const nameFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) => call[0]?.includes('workflow.name'), ); expect(nameFilterCalls).toHaveLength(0); @@ -151,7 +152,7 @@ describe('WorkflowRepository', () => { sharedWorkflowRepository, workflowHistoryRepository, ); - jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); + vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); const workflowIds = ['workflow1']; const options = { @@ -190,13 +191,13 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); - const andWhereCall = (queryBuilder.andWhere as jest.Mock).mock.calls.find((call) => + const andWhereCall = (queryBuilder.andWhere as Mock).mock.calls.find((call) => call[0]?.includes('workflow.name'), ); expect(andWhereCall).toBeDefined(); - expect(andWhereCall[0]).toContain('workflow.name'); - expect(andWhereCall[0]).toContain('workflow.description'); + expect(andWhereCall![0]).toContain('workflow.name'); + expect(andWhereCall![0]).toContain('workflow.description'); }); it('should handle special characters in search query', async () => { @@ -293,7 +294,7 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, {}); - const leftJoinCalls = (queryBuilder.leftJoin as jest.Mock).mock.calls.filter( + const leftJoinCalls = (queryBuilder.leftJoin as Mock).mock.calls.filter( (call) => call[0] === 'workflow.activeVersion', ); expect(leftJoinCalls).toHaveLength(0); @@ -307,7 +308,7 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); - const leftJoinCalls = (queryBuilder.leftJoin as jest.Mock).mock.calls.filter( + const leftJoinCalls = (queryBuilder.leftJoin as Mock).mock.calls.filter( (call) => call[0] === 'workflow.activeVersion', ); expect(leftJoinCalls).toHaveLength(0); @@ -372,7 +373,7 @@ describe('WorkflowRepository', () => { sharedWorkflowRepository, workflowHistoryRepository, ); - jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); + vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); const workflowIds = ['workflow1']; const options = { @@ -401,7 +402,7 @@ describe('WorkflowRepository', () => { sharedWorkflowRepository, workflowHistoryRepository, ); - jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); + vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); const workflowIds = ['workflow1']; const options = { @@ -438,7 +439,7 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); // leftJoin should not be called for activeVersion since it's already joined - const activeVersionJoinCalls = (queryBuilder.leftJoin as jest.Mock).mock.calls.filter( + const activeVersionJoinCalls = (queryBuilder.leftJoin as Mock).mock.calls.filter( (call) => call[0] === 'workflow.activeVersion', ); expect(activeVersionJoinCalls).toHaveLength(0); @@ -458,7 +459,7 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); - const triggerFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) => + const triggerFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) => call[0]?.includes?.('triggerNodeType'), ); expect(triggerFilterCalls).toHaveLength(0); @@ -472,7 +473,7 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); - const triggerFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) => + const triggerFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) => call[0]?.includes?.('triggerNodeType'), ); expect(triggerFilterCalls).toHaveLength(0); @@ -486,7 +487,7 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); - const triggerFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) => + const triggerFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) => call[0]?.includes?.('triggerNodeType'), ); expect(triggerFilterCalls).toHaveLength(0); @@ -505,10 +506,10 @@ describe('WorkflowRepository', () => { await workflowRepository.getMany(workflowIds, options); // Should have called andWhere for both name and triggerNodeTypes filters - const nameFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) => + const nameFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) => call[0]?.includes?.('workflow.name'), ); - const triggerFilterCalls = (queryBuilder.andWhere as jest.Mock).mock.calls.filter((call) => + const triggerFilterCalls = (queryBuilder.andWhere as Mock).mock.calls.filter((call) => call[0]?.includes?.('triggerNodeType'), ); expect(nameFilterCalls.length).toBeGreaterThan(0); @@ -521,7 +522,7 @@ describe('WorkflowRepository', () => { describe('findByIds', () => { it('should return an empty array and not call the database when no workflow ids are provided', async () => { - const findSpy = jest.spyOn(workflowRepository, 'find'); + const findSpy = vi.spyOn(workflowRepository, 'find'); const workflowIds: string[] = []; const result = await workflowRepository.findByIds(workflowIds); @@ -530,7 +531,7 @@ describe('WorkflowRepository', () => { }); it('should call the database when workflow ids are provided', async () => { - const findSpy = jest.spyOn(workflowRepository, 'find').mockResolvedValue([]); + const findSpy = vi.spyOn(workflowRepository, 'find').mockResolvedValue([]); const workflowIds = ['workflow1']; const result = await workflowRepository.findByIds(workflowIds); expect(result).toEqual([]); @@ -601,7 +602,7 @@ describe('WorkflowRepository', () => { sharedWorkflowRepository, workflowHistoryRepository, ); - jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); + vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(queryBuilder); queryBuilder.getMany.mockResolvedValue([]); const result = await sqliteWorkflowRepository.findByCredentialResolverId('resolver-123'); @@ -624,13 +625,13 @@ describe('WorkflowRepository', () => { describe('clearCredentialResolverId', () => { it('should use PostgreSQL jsonb removal for postgresdb', async () => { - const mockExecute = jest.fn().mockResolvedValue({ affected: 1 }); - const mockUpdateWhere = jest.fn().mockReturnValue({ execute: mockExecute }); - const mockSet = jest.fn().mockReturnValue({ where: mockUpdateWhere }); - const mockUpdate = jest.fn().mockReturnValue({ set: mockSet }); + const mockExecute = vi.fn().mockResolvedValue({ affected: 1 }); + const mockUpdateWhere = vi.fn().mockReturnValue({ execute: mockExecute }); + const mockSet = vi.fn().mockReturnValue({ where: mockUpdateWhere }); + const mockUpdate = vi.fn().mockReturnValue({ set: mockSet }); const updateQb = { update: mockUpdate } as unknown as SelectQueryBuilder; - jest.spyOn(workflowRepository, 'createQueryBuilder').mockReturnValue(updateQb); + vi.spyOn(workflowRepository, 'createQueryBuilder').mockReturnValue(updateQb); await workflowRepository.clearCredentialResolverId('resolver-123'); @@ -646,10 +647,10 @@ describe('WorkflowRepository', () => { }); it('should use SQLite json_remove for sqlite', async () => { - const mockExecute = jest.fn().mockResolvedValue({ affected: 1 }); - const mockUpdateWhere = jest.fn().mockReturnValue({ execute: mockExecute }); - const mockSet = jest.fn().mockReturnValue({ where: mockUpdateWhere }); - const mockUpdate = jest.fn().mockReturnValue({ set: mockSet }); + const mockExecute = vi.fn().mockResolvedValue({ affected: 1 }); + const mockUpdateWhere = vi.fn().mockReturnValue({ execute: mockExecute }); + const mockSet = vi.fn().mockReturnValue({ where: mockUpdateWhere }); + const mockUpdate = vi.fn().mockReturnValue({ set: mockSet }); const updateQb = { update: mockUpdate } as unknown as SelectQueryBuilder; const sqliteConfig = mockInstance(GlobalConfig, { @@ -662,7 +663,7 @@ describe('WorkflowRepository', () => { sharedWorkflowRepository, workflowHistoryRepository, ); - jest.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(updateQb); + vi.spyOn(sqliteWorkflowRepository, 'createQueryBuilder').mockReturnValue(updateQb); await sqliteWorkflowRepository.clearCredentialResolverId('resolver-123'); diff --git a/packages/@n8n/db/src/services/__tests__/db-lock.service.test.ts b/packages/@n8n/db/src/services/__tests__/db-lock.service.test.ts index f71f8ffdbb4..d6abb0e3cfb 100644 --- a/packages/@n8n/db/src/services/__tests__/db-lock.service.test.ts +++ b/packages/@n8n/db/src/services/__tests__/db-lock.service.test.ts @@ -1,8 +1,8 @@ import type { DatabaseConfig } from '@n8n/config'; import { QueryFailedError } from '@n8n/typeorm'; import type { DataSource, EntityManager } from '@n8n/typeorm'; -import { mock } from 'jest-mock-extended'; import { OperationalError } from 'n8n-workflow'; +import { mock } from 'vitest-mock-extended'; import { DbLockService } from '../db-lock.service'; @@ -11,12 +11,13 @@ describe('DbLockService', () => { const dataSource = mock(); const databaseConfig = mock(); - const transactionMock = jest.fn, [(tx: EntityManager) => Promise]>(); + const transactionMock = + vi.fn<(...args: [(tx: EntityManager) => Promise]) => Promise>(); let service: DbLockService; beforeEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); transactionMock.mockImplementation(async (fn) => await fn(mockTx)); dataSource.manager.transaction = transactionMock as never; mockTx.query.mockResolvedValue([]); @@ -26,7 +27,7 @@ describe('DbLockService', () => { describe('withLock', () => { it('should acquire advisory lock and execute fn on Postgres', async () => { databaseConfig.type = 'postgresdb'; - const fn = jest.fn().mockResolvedValue('result'); + const fn = vi.fn().mockResolvedValue('result'); const result = await service.withLock(1001, fn); @@ -37,7 +38,7 @@ describe('DbLockService', () => { it('should skip advisory lock on SQLite and still execute fn', async () => { databaseConfig.type = 'sqlite'; - const fn = jest.fn().mockResolvedValue('result'); + const fn = vi.fn().mockResolvedValue('result'); const result = await service.withLock(1001, fn); @@ -48,7 +49,7 @@ describe('DbLockService', () => { it('should set lock_timeout when timeoutMs is provided', async () => { databaseConfig.type = 'postgresdb'; - const fn = jest.fn().mockResolvedValue('result'); + const fn = vi.fn().mockResolvedValue('result'); await service.withLock(1001, fn, { timeoutMs: 5000 }); @@ -58,7 +59,7 @@ describe('DbLockService', () => { it('should not set lock_timeout when timeoutMs is not provided', async () => { databaseConfig.type = 'postgresdb'; - const fn = jest.fn().mockResolvedValue('result'); + const fn = vi.fn().mockResolvedValue('result'); await service.withLock(1001, fn); @@ -68,7 +69,7 @@ describe('DbLockService', () => { it('should throw OperationalError when lock timeout is exceeded', async () => { databaseConfig.type = 'postgresdb'; - const fn = jest.fn(); + const fn = vi.fn(); const timeoutError = new QueryFailedError( 'SELECT pg_advisory_xact_lock($1)', [1001], @@ -86,7 +87,7 @@ describe('DbLockService', () => { it('should include timeout details in OperationalError message', async () => { databaseConfig.type = 'postgresdb'; - const fn = jest.fn(); + const fn = vi.fn(); const timeoutError = new QueryFailedError( 'SELECT pg_advisory_xact_lock($1)', [1001], @@ -102,7 +103,7 @@ describe('DbLockService', () => { it('should propagate non-timeout errors unchanged', async () => { databaseConfig.type = 'postgresdb'; - const fn = jest.fn(); + const fn = vi.fn(); const otherError = new Error('connection lost'); mockTx.query.mockRejectedValueOnce(otherError); @@ -115,7 +116,7 @@ describe('DbLockService', () => { describe('tryWithLock', () => { it('should execute fn when lock is acquired', async () => { databaseConfig.type = 'postgresdb'; - const fn = jest.fn().mockResolvedValue('result'); + const fn = vi.fn().mockResolvedValue('result'); mockTx.query.mockResolvedValueOnce([{ pg_try_advisory_xact_lock: true }]); const result = await service.tryWithLock(1001, fn); @@ -127,7 +128,7 @@ describe('DbLockService', () => { it('should throw OperationalError when lock is already held', async () => { databaseConfig.type = 'postgresdb'; - const fn = jest.fn(); + const fn = vi.fn(); mockTx.query.mockResolvedValueOnce([{ pg_try_advisory_xact_lock: false }]); const error = await service.tryWithLock(1001, fn).catch((e: unknown) => e); @@ -140,7 +141,7 @@ describe('DbLockService', () => { it('should skip advisory lock on SQLite and execute fn', async () => { databaseConfig.type = 'sqlite'; - const fn = jest.fn().mockResolvedValue('result'); + const fn = vi.fn().mockResolvedValue('result'); const result = await service.tryWithLock(1001, fn); @@ -156,7 +157,7 @@ describe('DbLockService', () => { }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); it('should serialize concurrent withLock calls for the same lockId', async () => { @@ -166,13 +167,13 @@ describe('DbLockService', () => { resolveFirst = r; }); - const fn1 = jest.fn(async () => { + const fn1 = vi.fn(async () => { executionOrder.push('fn1-start'); await firstBlocking; executionOrder.push('fn1-end'); return 'first'; }); - const fn2 = jest.fn(async () => { + const fn2 = vi.fn(async () => { executionOrder.push('fn2-start'); return 'second'; }); @@ -199,11 +200,11 @@ describe('DbLockService', () => { resolveFirst = r; }); - const fn1 = jest.fn(async () => { + const fn1 = vi.fn(async () => { await firstBlocking; return 'first'; }); - const fn2 = jest.fn(async () => 'second'); + const fn2 = vi.fn(async () => 'second'); const p1 = service.withLock(1001, fn1); const p2 = service.withLock(9999, fn2); @@ -220,23 +221,23 @@ describe('DbLockService', () => { }); it('should reject with OperationalError when withLock timeout expires', async () => { - jest.useFakeTimers(); + vi.useFakeTimers(); let resolveFirst!: () => void; const firstBlocking = new Promise((r) => { resolveFirst = r; }); - const fn1 = jest.fn(async () => { + const fn1 = vi.fn(async () => { await firstBlocking; return 'first'; }); - const fn2 = jest.fn().mockResolvedValue('second'); + const fn2 = vi.fn().mockResolvedValue('second'); const p1 = service.withLock(1001, fn1); const p2 = service.withLock(1001, fn2, { timeoutMs: 100 }); - jest.advanceTimersByTime(100); + vi.advanceTimersByTime(100); await expect(p2).rejects.toThrow(OperationalError); await expect(p2).rejects.toThrow(/Timed out waiting for DbLock 1001 after 100ms/); @@ -252,11 +253,11 @@ describe('DbLockService', () => { resolveFirst = r; }); - const fn1 = jest.fn(async () => { + const fn1 = vi.fn(async () => { await firstBlocking; return 'first'; }); - const fn2 = jest.fn().mockResolvedValue('second'); + const fn2 = vi.fn().mockResolvedValue('second'); const p1 = service.withLock(1001, fn1); @@ -273,8 +274,8 @@ describe('DbLockService', () => { }); it('should release lock when fn throws in withLock', async () => { - const fn1 = jest.fn().mockRejectedValue(new Error('fn1 failed')); - const fn2 = jest.fn().mockResolvedValue('second'); + const fn1 = vi.fn().mockRejectedValue(new Error('fn1 failed')); + const fn2 = vi.fn().mockResolvedValue('second'); await expect(service.withLock(1001, fn1)).rejects.toThrow('fn1 failed'); @@ -283,8 +284,8 @@ describe('DbLockService', () => { }); it('should release lock when fn throws in tryWithLock', async () => { - const fn1 = jest.fn().mockRejectedValue(new Error('fn1 failed')); - const fn2 = jest.fn().mockResolvedValue('second'); + const fn1 = vi.fn().mockRejectedValue(new Error('fn1 failed')); + const fn2 = vi.fn().mockResolvedValue('second'); await expect(service.tryWithLock(1001, fn1)).rejects.toThrow('fn1 failed'); @@ -298,12 +299,12 @@ describe('DbLockService', () => { resolveFirst = r; }); - const fn1 = jest.fn(async () => { + const fn1 = vi.fn(async () => { await firstBlocking; return 'first'; }); - const fn2 = jest.fn().mockResolvedValue('second'); - const fn3 = jest.fn().mockResolvedValue('third'); + const fn2 = vi.fn().mockResolvedValue('second'); + const fn3 = vi.fn().mockResolvedValue('third'); const p1 = service.withLock(1001, fn1); const p2 = service.withLock(1001, fn2); @@ -331,17 +332,17 @@ describe('DbLockService', () => { resolve2 = r; }); - const fn1 = jest.fn(async () => { + const fn1 = vi.fn(async () => { executionOrder.push('fn1'); await blocking1; return 'first'; }); - const fn2 = jest.fn(async () => { + const fn2 = vi.fn(async () => { executionOrder.push('fn2'); await blocking2; return 'second'; }); - const fn3 = jest.fn(async () => { + const fn3 = vi.fn(async () => { executionOrder.push('fn3'); return 'third'; }); diff --git a/packages/@n8n/db/src/utils/__tests__/apply-workflow-boolean-setting-filter.test.ts b/packages/@n8n/db/src/utils/__tests__/apply-workflow-boolean-setting-filter.test.ts index e53c3db90a8..b2d2e5981ed 100644 --- a/packages/@n8n/db/src/utils/__tests__/apply-workflow-boolean-setting-filter.test.ts +++ b/packages/@n8n/db/src/utils/__tests__/apply-workflow-boolean-setting-filter.test.ts @@ -5,9 +5,9 @@ import { applyWorkflowBooleanSettingFilter } from '../apply-workflow-boolean-set function createMockQb() { const qb = { - andWhere: jest.fn(), - where: jest.fn(), - orWhere: jest.fn(), + andWhere: vi.fn(), + where: vi.fn(), + orWhere: vi.fn(), } as unknown as SelectQueryBuilder; return qb; } diff --git a/packages/@n8n/db/src/utils/__tests__/get-test-run-final-result.ee.test.ts b/packages/@n8n/db/src/utils/__tests__/get-test-run-final-result.ee.test.ts index f16ef9cceb6..44d88609201 100644 --- a/packages/@n8n/db/src/utils/__tests__/get-test-run-final-result.ee.test.ts +++ b/packages/@n8n/db/src/utils/__tests__/get-test-run-final-result.ee.test.ts @@ -1,4 +1,4 @@ -import { mock } from 'jest-mock-extended'; +import { mock } from 'vitest-mock-extended'; import type { TestCaseExecution } from '../../entities'; import { getTestRunFinalResult } from '../get-final-test-result'; diff --git a/packages/@n8n/db/src/utils/test-utils/mock-entity-manager.ts b/packages/@n8n/db/src/utils/test-utils/mock-entity-manager.ts index 9f434f91d5b..de6daf207e2 100644 --- a/packages/@n8n/db/src/utils/test-utils/mock-entity-manager.ts +++ b/packages/@n8n/db/src/utils/test-utils/mock-entity-manager.ts @@ -1,6 +1,6 @@ import { DataSource, EntityManager, type EntityMetadata } from '@n8n/typeorm'; -import { mock } from 'jest-mock-extended'; import type { Class } from 'n8n-core'; +import { mock } from 'vitest-mock-extended'; import { mockInstance } from './mock-instance'; diff --git a/packages/@n8n/db/src/utils/test-utils/mock-instance.ts b/packages/@n8n/db/src/utils/test-utils/mock-instance.ts index af6aa4cb624..d5f73e02f3d 100644 --- a/packages/@n8n/db/src/utils/test-utils/mock-instance.ts +++ b/packages/@n8n/db/src/utils/test-utils/mock-instance.ts @@ -1,5 +1,5 @@ import { Container, type Constructable } from '@n8n/di'; -import { mock } from 'jest-mock-extended'; +import { mock } from 'vitest-mock-extended'; export const mockInstance = ( serviceClass: Constructable, diff --git a/packages/@n8n/db/tsconfig.build.json b/packages/@n8n/db/tsconfig.build.json index ee0e3e20fda..c45aa087e59 100644 --- a/packages/@n8n/db/tsconfig.build.json +++ b/packages/@n8n/db/tsconfig.build.json @@ -7,5 +7,5 @@ "tsBuildInfoFile": "dist/build.tsbuildinfo" }, "include": ["src/**/*.ts"], - "exclude": ["src/**/__tests__/**"] + "exclude": ["src/**/__tests__/**", "src/**/*.test.ts", "src/**/*.spec.ts"] } diff --git a/packages/@n8n/db/tsconfig.json b/packages/@n8n/db/tsconfig.json index 864706c244c..43e4621d711 100644 --- a/packages/@n8n/db/tsconfig.json +++ b/packages/@n8n/db/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "@n8n/typescript-config/tsconfig.common.json", "compilerOptions": { - "types": ["node", "jest"], + "types": ["node", "vitest/globals"], "tsBuildInfoFile": "dist/typecheck.tsbuildinfo", "experimentalDecorators": true, "emitDecoratorMetadata": true, diff --git a/packages/@n8n/db/vite.config.ts b/packages/@n8n/db/vite.config.ts new file mode 100644 index 00000000000..5c2e042621a --- /dev/null +++ b/packages/@n8n/db/vite.config.ts @@ -0,0 +1,156 @@ +import { createVitestConfigWithDecorators } from '@n8n/vitest-config/node-decorators'; +import fs from 'node:fs'; +import path from 'node:path'; +import ts from 'typescript'; +import { mergeConfig, type Plugin } from 'vite'; +import { configDefaults } from 'vitest/config'; + +/** + * Vite plugin that transpiles this package's TypeORM entity files (`src/entities/**`) + * through the real TypeScript compiler (a full `ts.LanguageService`, not single-file + * `transpileModule`). Every other source file is left to Vite's fast oxc transform. + * + * TypeORM entities rely on `emitDecoratorMetadata` to derive column types from the + * reflected `design:type`. For a string-literal union column (e.g. + * `providerType: AuthProviderType`, where the alias is imported from another file), + * only `tsc` with cross-file type information collapses the union to `String`. Vite's + * oxc transform — and SWC — emit `Object` instead, which TypeORM rejects at + * `DataSource.initialize()`. Single-file `transpileModule` also emits `Object` because + * it can't resolve the imported alias. A full Program is required, which mirrors the + * old jest config that set `isolatedModules: false` for exactly this reason. + * + * Scoping to `src/entities/**` keeps the cost contained: only the ~50 entity files pay + * the tsc price (and the Program is rooted there), while DI `@Service` constructor + * metadata — which oxc emits correctly — keeps the fast path for the rest of `src`. + */ +function tscDecoratorTransform(): Plugin { + const projectDir = __dirname; + const entitiesDir = path.resolve(projectDir, 'src', 'entities') + path.sep; + let emit: ((fileName: string) => { code: string; map: unknown } | null) | undefined; + + function createEmitter() { + const configPath = ts.findConfigFile(projectDir, ts.sys.fileExists, 'tsconfig.json'); + if (!configPath) { + throw new Error('Could not find tsconfig.json for @n8n/db'); + } + + const { config } = ts.readConfigFile(configPath, ts.sys.readFile); + const parsed = ts.parseJsonConfigFileContent(config, ts.sys, projectDir); + // Root the Program at the entity files only; their imported types (e.g. the union + // aliases in `types-db.ts`, related entities) are still resolved on demand via the + // host's snapshot reads, so cross-file metadata stays correct. + const rootFiles = parsed.fileNames.filter((f) => path.normalize(f).startsWith(entitiesDir)); + + const options: ts.CompilerOptions = { + ...parsed.options, + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2022, + moduleResolution: ts.ModuleResolutionKind.Bundler, + experimentalDecorators: true, + emitDecoratorMetadata: true, + verbatimModuleSyntax: false, + isolatedModules: false, + sourceMap: true, + inlineSources: true, + skipLibCheck: true, + noEmit: false, + noEmitOnError: false, + declaration: false, + declarationMap: false, + composite: false, + incremental: false, + tsBuildInfoFile: undefined, + }; + + const versions = new Map(); + const contents = new Map(); + for (const f of rootFiles) { + versions.set(path.normalize(f), 0); + } + + // Re-read `fileName` from disk and, if its content changed since the last read, + // bump the script version so the LanguageService invalidates its cached emit. + function refresh(fileName: string): string | undefined { + const norm = path.normalize(fileName); + const next = fs.existsSync(norm) ? fs.readFileSync(norm, 'utf-8') : undefined; + if (next !== contents.get(norm)) { + if (next === undefined) { + contents.delete(norm); + } else { + contents.set(norm, next); + } + + versions.set(norm, (versions.get(norm) ?? 0) + 1); + } + return next; + } + + const host: ts.LanguageServiceHost = { + getScriptFileNames: () => Array.from(versions.keys()), + getScriptVersion: (f) => String(versions.get(path.normalize(f)) ?? 0), + getScriptSnapshot: (f) => { + const cached = contents.get(path.normalize(f)); + const text = cached ?? (fs.existsSync(f) ? fs.readFileSync(f, 'utf-8') : undefined); + return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text); + }, + getCurrentDirectory: () => projectDir, + getCompilationSettings: () => options, + getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o), + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + readDirectory: ts.sys.readDirectory, + directoryExists: ts.sys.directoryExists, + getDirectories: ts.sys.getDirectories, + }; + + const service = ts.createLanguageService(host, ts.createDocumentRegistry()); + + return (fileName: string) => { + const norm = path.normalize(fileName); + // Pick up on-disk edits (watch mode) by bumping the script version when the + // content changes; otherwise the LanguageService reuses a stale cached emit. + if (refresh(norm) === undefined) { + return null; + } + + const output = service.getEmitOutput(norm); + const js = output.outputFiles.find((f) => f.name.endsWith('.js')); + const map = output.outputFiles.find((f) => f.name.endsWith('.js.map')); + if (!js) { + return null; + } + + return { code: js.text, map: map ? (JSON.parse(map.text) as unknown) : null }; + }; + } + + return { + name: 'tsc-decorator-transform', + enforce: 'pre', + transform(_code, id) { + const file = id.split('?')[0]; + if (!file.startsWith(entitiesDir) || !/\.tsx?$/.test(file)) return null; + emit ??= createEmitter(); + const result = emit(file); + return result ? { code: result.code, map: result.map as never } : null; + }, + }; +} + +export default mergeConfig( + createVitestConfigWithDecorators({ + // The n8n root jest.config sets `restoreMocks: true`, and most test files silently + // rely on it — omit this and mocks bleed between tests. + restoreMocks: true, + }), + { + plugins: [tscDecoratorTransform()], + test: { + // Vitest 4's default exclude is only node_modules/.git — it does NOT cover dist. + // Without this, compiled test files left in dist (tsc never deletes orphaned + // output) get collected and fail (CJS `require('vitest')`). The build also + // excludes test files now, but this guards against pre-existing stale artifacts. + exclude: [...configDefaults.exclude, '**/dist/**'], + }, + }, +); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6663a30b34..cf1790d4125 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1531,12 +1531,27 @@ importers: '@n8n/typescript-config': specifier: workspace:* version: link:../typescript-config + '@n8n/vitest-config': + specifier: workspace:* + version: link:../vitest-config '@types/lodash': specifier: 'catalog:' version: 4.17.17 + '@vitest/coverage-v8': + specifier: 'catalog:' + version: 4.1.1(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) express: specifier: 5.1.0 version: 5.1.0 + typescript: + specifier: 6.0.2 + version: 6.0.2 + vitest: + specifier: 'catalog:' + version: 4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3)) + vitest-mock-extended: + specifier: 'catalog:' + version: 3.1.0(typescript@6.0.2)(vitest@4.1.1(@opentelemetry/api@1.9.0)(@types/node@20.19.41)(jsdom@23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))(vite@8.0.2(@types/node@20.19.41)(esbuild@0.25.10)(jiti@2.6.1)(sass-embedded@1.98.0)(sass@1.98.0)(terser@5.16.1)(tsx@4.19.3)(yaml@2.8.3))) packages/@n8n/decorators: dependencies: