diff --git a/.eslintrc.js b/.eslintrc.js index 1ba3db8655..b3761c4a2b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,12 +23,13 @@ module.exports = { ecmaVersion: 2020, }, overrides: [ - // Parse rollup configuration as module + // Parse config files as modules { - files: ['rollup.config.js', 'vite.config.js'], + files: ['rollup.config.js', 'vite?(st).config.js', 'api/globalSetup.js'], parserOptions: { sourceType: 'module', }, + rules: defaultRules, }, { files: ['**/*.test.js'], @@ -36,6 +37,7 @@ module.exports = { jest: true, }, plugins: ['jest'], + rules: defaultRules, }, // Configuration for ts/vue files { diff --git a/api/globalSetup.js b/api/globalSetup.js new file mode 100644 index 0000000000..57b3cc0c35 --- /dev/null +++ b/api/globalSetup.js @@ -0,0 +1,7 @@ +// From https://sharp.pixelplumbing.com/install#worker-threads: +// On some platforms, including glibc-based Linux, the main thread must call require('sharp') before worker threads are created. +// This is to ensure shared libraries remain loaded in memory until after all threads are complete. +// Without this, the following error may occur: Module did not self-register +import 'sharp'; + +export default function () {} diff --git a/api/jest.config.js b/api/jest.config.js deleted file mode 100644 index d52598401c..0000000000 --- a/api/jest.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const base = require('../jest.config.js'); - -require('dotenv').config(); - -module.exports = { - ...base, - roots: ['/src'], - verbose: true, - setupFiles: ['dotenv/config'], - collectCoverageFrom: ['src/**/*.ts'], - testEnvironmentOptions: { - url: process.env.TEST_URL || 'http://localhost', - }, -}; diff --git a/api/package.json b/api/package.json index 4dc9ce0214..b53fb92ffc 100644 --- a/api/package.json +++ b/api/package.json @@ -63,9 +63,9 @@ "cleanup": "rimraf dist", "dev": "cross-env NODE_ENV=development SERVE_APP=false ts-node-dev --files --transpile-only --respawn --watch \".env\" --inspect=0 --exit-child -- src/start.ts", "cli": "cross-env NODE_ENV=development SERVE_APP=false ts-node --script-mode --transpile-only src/cli/run.ts", - "test": "jest", - "test:coverage": "jest --coverage", - "test:watch": "jest --watch" + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest" }, "engines": { "node": ">=12.20.0" @@ -197,7 +197,6 @@ "@types/flat": "5.0.2", "@types/fs-extra": "9.0.13", "@types/inquirer": "8.2.1", - "@types/jest": "29.2.0", "@types/js-yaml": "4.0.5", "@types/json2csv": "5.0.3", "@types/jsonwebtoken": "8.5.9", @@ -221,16 +220,16 @@ "@types/uuid": "8.3.4", "@types/uuid-validate": "0.0.1", "@types/wellknown": "0.5.3", + "@vitest/coverage-c8": "^0.25.1", "copyfiles": "2.4.1", "cross-env": "7.0.3", "form-data": "4.0.0", - "jest": "29.2.1", "knex-mock-client": "1.8.4", "rimraf": "3.0.2", "supertest": "6.3.0", - "ts-jest": "29.0.3", "ts-node": "10.9.1", "ts-node-dev": "2.0.0", - "typescript": "4.8.4" + "typescript": "4.8.4", + "vitest": "0.25.1" } } diff --git a/api/src/__mocks__/cache.ts b/api/src/__mocks__/cache.ts index 0cfd1be900..e395074913 100644 --- a/api/src/__mocks__/cache.ts +++ b/api/src/__mocks__/cache.ts @@ -1,4 +1,5 @@ -export const getCache = jest.fn().mockReturnValue({ cache: undefined, systemCache: undefined, lockCache: undefined }); -export const flushCaches = jest.fn(); -export const clearSystemCache = jest.fn(); -export const setSystemCache = jest.fn(); +import { vi } from 'vitest'; +export const getCache = vi.fn().mockReturnValue({ cache: undefined, systemCache: undefined, lockCache: undefined }); +export const flushCaches = vi.fn(); +export const clearSystemCache = vi.fn(); +export const setSystemCache = vi.fn(); diff --git a/api/src/cli/index.test.ts b/api/src/cli/index.test.ts index 75ea6608f5..91da2a3209 100644 --- a/api/src/cli/index.test.ts +++ b/api/src/cli/index.test.ts @@ -1,39 +1,48 @@ -import path from 'path'; import { Command } from 'commander'; import { Extension, HookConfig } from '@directus/shared/types'; import { createCli } from './index'; +import path from 'path'; +import { test, describe, expect, vi, beforeEach } from 'vitest'; -jest.mock('../../src/env', () => ({ - ...jest.requireActual('../../src/env').default, - EXTENSIONS_PATH: '', - SERVE_APP: false, - DB_CLIENT: 'pg', - DB_HOST: 'localhost', - DB_PORT: 5432, - DB_DATABASE: 'directus', - DB_USER: 'postgres', - DB_PASSWORD: 'psql1234', -})); +vi.mock('../../src/env', async () => { + const actual = (await vi.importActual('../../src/env')) as { default: Record }; -jest.mock('@directus/shared/utils/node/get-extensions', () => ({ - getPackageExtensions: jest.fn(() => Promise.resolve([])), - getLocalExtensions: jest.fn(() => Promise.resolve([customCliExtension])), -})); + return { + default: { + ...actual.default, + EXTENSIONS_PATH: '', + SERVE_APP: false, + DB_CLIENT: 'pg', + DB_HOST: 'localhost', + DB_PORT: 5432, + DB_DATABASE: 'directus', + DB_USER: 'postgres', + DB_PASSWORD: 'psql1234', + }, + }; +}); -const customHookPath = path.resolve('/hooks/custom-cli', 'index.js'); -jest.doMock(customHookPath, () => customCliHook, { virtual: true }); +vi.mock('@directus/shared/utils/node', async () => { + const actual = await vi.importActual('@directus/shared/utils/node'); -const customCliExtension: Extension = { - path: `/hooks/custom-cli`, - name: 'custom-cli', - type: 'hook', - entrypoint: 'index.js', - local: true, -}; + const customCliExtension: Extension = { + path: '/hooks/custom-cli', + name: 'custom-cli', + type: 'hook', + entrypoint: 'index.js', + local: true, + }; -const beforeHook = jest.fn(); -const afterAction = jest.fn(); -const afterHook = jest.fn(({ program }) => { + return { + ...(actual as object), + getPackageExtensions: vi.fn(() => Promise.resolve([])), + getLocalExtensions: vi.fn(() => Promise.resolve([customCliExtension])), + }; +}); + +const beforeHook = vi.fn(); +const afterAction = vi.fn(); +const afterHook = vi.fn(({ program }) => { (program as Command).command('custom').action(afterAction); }); @@ -42,8 +51,12 @@ const customCliHook: HookConfig = ({ init }) => { init('cli.after', afterHook); }; -const writeOut = jest.fn(); -const writeErr = jest.fn(); +vi.mock(path.resolve('/hooks/custom-cli', 'index.js'), () => ({ + default: customCliHook, +})); + +const writeOut = vi.fn(); +const writeErr = vi.fn(); const setup = async () => { const program = await createCli(); @@ -52,7 +65,9 @@ const setup = async () => { return program; }; -beforeEach(jest.clearAllMocks); +beforeEach(() => { + vi.clearAllMocks(); +}); describe('cli hooks', () => { test('should call hooks before and after creating the cli', async () => { diff --git a/api/src/controllers/files.test.ts b/api/src/controllers/files.test.ts index 0afc445584..938bcf7158 100644 --- a/api/src/controllers/files.test.ts +++ b/api/src/controllers/files.test.ts @@ -1,14 +1,14 @@ // @ts-nocheck -jest.mock('../../src/cache'); -jest.mock('../../src/database'); -jest.mock('../../src/utils/validate-env'); - import { multipartHandler } from './files'; import { InvalidPayloadException } from '../exceptions/invalid-payload'; import { PassThrough } from 'stream'; - import FormData from 'form-data'; +import { vi, describe, expect, it } from 'vitest'; + +vi.mock('../../src/cache'); +vi.mock('../../src/database'); +vi.mock('../../src/utils/validate-env'); describe('multipartHandler', () => { it(`Errors out if request doesn't contain any files to upload`, () => { @@ -18,7 +18,7 @@ describe('multipartHandler', () => { const req = { headers: fakeForm.getHeaders(), - is: jest.fn().mockReturnValue(true), + is: vi.fn().mockReturnValue(true), body: fakeForm.getBuffer(), params: {}, pipe: (input) => stream.pipe(input), @@ -45,7 +45,7 @@ describe('multipartHandler', () => { const req = { headers: fakeForm.getHeaders(), - is: jest.fn().mockReturnValue(true), + is: vi.fn().mockReturnValue(true), body: fakeForm.getBuffer(), params: {}, pipe: (input) => stream.pipe(input), diff --git a/api/src/database/migrations/run.test.ts b/api/src/database/migrations/run.test.ts index db688d37ea..126e214394 100644 --- a/api/src/database/migrations/run.test.ts +++ b/api/src/database/migrations/run.test.ts @@ -1,13 +1,14 @@ import knex, { Knex } from 'knex'; import { getTracker, MockClient, Tracker } from 'knex-mock-client'; import run from './run'; +import { describe, beforeAll, afterEach, it, expect } from 'vitest'; describe('run', () => { - let db: jest.Mocked; + let db: Knex; let tracker: Tracker; beforeAll(() => { - db = knex({ client: MockClient }) as jest.Mocked; + db = knex({ client: MockClient }); tracker = getTracker(); }); diff --git a/api/src/database/migrations/run.ts b/api/src/database/migrations/run.ts index 5efc8783a0..db722e7301 100644 --- a/api/src/database/migrations/run.ts +++ b/api/src/database/migrations/run.ts @@ -63,7 +63,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la throw Error('Nothing to upgrade'); } - const { up } = require(nextVersion.file); + const { up } = await import(nextVersion.file); if (log) { logger.info(`Applying ${nextVersion.name}...`); @@ -86,7 +86,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la throw new Error("Couldn't find migration"); } - const { down } = require(migration.file); + const { down } = await import(migration.file); if (log) { logger.info(`Undoing ${migration.name}...`); @@ -99,7 +99,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la async function latest() { for (const migration of migrations) { if (migration.completed === false) { - const { up } = require(migration.file); + const { up } = await import(migration.file); if (log) { logger.info(`Applying ${migration.name}...`); diff --git a/api/src/env.test.ts b/api/src/env.test.ts index e87ee47288..9b381eaf47 100644 --- a/api/src/env.test.ts +++ b/api/src/env.test.ts @@ -1,3 +1,5 @@ +import { describe, test, expect, vi } from 'vitest'; + const testEnv = { NUMBER: '1234', NUMBER_CAST_AS_STRING: 'string:1234', @@ -7,20 +9,9 @@ const testEnv = { MULTIPLE: 'array:string:https://example.com,regex:\\.example2\\.com$', }; -describe('env processed values', () => { - const originalEnv = process.env; - let env: Record; - - beforeEach(() => { - jest.resetModules(); - process.env = { ...testEnv }; - env = jest.requireActual('../src/env').default; - }); - - afterEach(() => { - process.env = originalEnv; - jest.resetAllMocks(); - }); +describe('env processed values', async () => { + process.env = { ...testEnv }; + const env = ((await vi.importActual('../src/env')) as { default: Record }).default; test('Number value should be a number', () => { expect(env.NUMBER).toStrictEqual(1234); diff --git a/api/src/extensions.ts b/api/src/extensions.ts index 8fa1d10f02..4777c0e8fe 100644 --- a/api/src/extensions.ts +++ b/api/src/extensions.ts @@ -197,7 +197,7 @@ class ExtensionManager { logger.warn(err); } - this.registerHooks(); + await this.registerHooks(); this.registerEndpoints(); await this.registerOperations(); @@ -341,13 +341,12 @@ class ExtensionManager { return depsMapping; } - private registerHooks(): void { + private async registerHooks(): Promise { const hooks = this.extensions.filter((extension): extension is ApiExtension => extension.type === 'hook'); - for (const hook of hooks) { try { const hookPath = path.resolve(hook.path, hook.entrypoint); - const hookInstance: HookConfig | { default: HookConfig } = require(hookPath); + const hookInstance: HookConfig | { default: HookConfig } = await import(hookPath); const config = getModuleDefault(hookInstance); diff --git a/api/src/middleware/authenticate.test.ts b/api/src/middleware/authenticate.test.ts index f11d9fc6ae..f1af1f192a 100644 --- a/api/src/middleware/authenticate.test.ts +++ b/api/src/middleware/authenticate.test.ts @@ -7,28 +7,31 @@ import env from '../env'; import { InvalidCredentialsException } from '../exceptions'; import { handler } from './authenticate'; import '../../src/types/express.d.ts'; +import { vi, afterEach, test, expect } from 'vitest'; -jest.mock('../../src/database'); -jest.mock('../../src/env', () => ({ - SECRET: 'test', +vi.mock('../../src/database'); +vi.mock('../../src/env', () => ({ + default: { + SECRET: 'test', + }, })); afterEach(() => { - jest.resetAllMocks(); + vi.resetAllMocks(); }); test('Short-circuits when authenticate filter is used', async () => { const req = { ip: '127.0.0.1', - get: jest.fn(), + get: vi.fn(), }; const res = {}; - const next = jest.fn(); + const next = vi.fn(); const customAccountability = { admin: true }; - jest.spyOn(emitter, 'emitFilter').mockResolvedValue(customAccountability); + vi.spyOn(emitter, 'emitFilter').mockResolvedValue(customAccountability); await handler(req, res, next); @@ -39,7 +42,7 @@ test('Short-circuits when authenticate filter is used', async () => { test('Uses default public accountability when no token is given', async () => { const req = { ip: '127.0.0.1', - get: jest.fn((string) => { + get: vi.fn((string) => { switch (string) { case 'user-agent': return 'fake-user-agent'; @@ -52,9 +55,9 @@ test('Uses default public accountability when no token is given', async () => { }; const res = {}; - const next = jest.fn(); + const next = vi.fn(); - jest.spyOn(emitter, 'emitFilter').mockImplementation((_, payload) => payload); + vi.spyOn(emitter, 'emitFilter').mockImplementation((_, payload) => payload); await handler(req, res, next); @@ -97,7 +100,7 @@ test('Sets accountability to payload contents if valid token is passed', async ( const req = { ip: '127.0.0.1', - get: jest.fn((string) => { + get: vi.fn((string) => { switch (string) { case 'user-agent': return 'fake-user-agent'; @@ -111,7 +114,7 @@ test('Sets accountability to payload contents if valid token is passed', async ( }; const res = {}; - const next = jest.fn(); + const next = vi.fn(); await handler(req, res, next); @@ -163,17 +166,17 @@ test('Sets accountability to payload contents if valid token is passed', async ( }); test('Throws InvalidCredentialsException when static token is used, but user does not exist', async () => { - jest.mocked(getDatabase).mockReturnValue({ - select: jest.fn().mockReturnThis(), - from: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - first: jest.fn().mockResolvedValue(undefined), + vi.mocked(getDatabase).mockReturnValue({ + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + leftJoin: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + first: vi.fn().mockResolvedValue(undefined), }); const req = { ip: '127.0.0.1', - get: jest.fn((string) => { + get: vi.fn((string) => { switch (string) { case 'user-agent': return 'fake-user-agent'; @@ -187,7 +190,7 @@ test('Throws InvalidCredentialsException when static token is used, but user doe }; const res = {}; - const next = jest.fn(); + const next = vi.fn(); expect(handler(req, res, next)).rejects.toEqual(new InvalidCredentialsException()); expect(next).toHaveBeenCalledTimes(0); @@ -196,7 +199,7 @@ test('Throws InvalidCredentialsException when static token is used, but user doe test('Sets accountability to user information when static token is used', async () => { const req = { ip: '127.0.0.1', - get: jest.fn((string) => { + get: vi.fn((string) => { switch (string) { case 'user-agent': return 'fake-user-agent'; @@ -210,7 +213,7 @@ test('Sets accountability to user information when static token is used', async }; const res = {}; - const next = jest.fn(); + const next = vi.fn(); const testUser = { id: 'test-id', role: 'test-role', admin_access: true, app_access: false }; @@ -224,12 +227,12 @@ test('Sets accountability to user information when static token is used', async origin: 'fake-origin', }; - jest.mocked(getDatabase).mockReturnValue({ - select: jest.fn().mockReturnThis(), - from: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - where: jest.fn().mockReturnThis(), - first: jest.fn().mockResolvedValue(testUser), + vi.mocked(getDatabase).mockReturnValue({ + select: vi.fn().mockReturnThis(), + from: vi.fn().mockReturnThis(), + leftJoin: vi.fn().mockReturnThis(), + where: vi.fn().mockReturnThis(), + first: vi.fn().mockResolvedValue(testUser), }); await handler(req, res, next); diff --git a/api/src/middleware/extract-token.test.ts b/api/src/middleware/extract-token.test.ts index 7707f13936..1c5f16e74c 100644 --- a/api/src/middleware/extract-token.test.ts +++ b/api/src/middleware/extract-token.test.ts @@ -1,15 +1,16 @@ -import { NextFunction, Request, Response } from 'express'; +import { Request, Response } from 'express'; import extractToken from '../../src/middleware/extract-token'; import '../../src/types/express.d.ts'; +import { vi, beforeEach, test, expect } from 'vitest'; let mockRequest: Partial; let mockResponse: Partial; -const nextFunction: NextFunction = jest.fn(); +const nextFunction = vi.fn(); beforeEach(() => { mockRequest = {}; mockResponse = {}; - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('Token from query', () => { diff --git a/api/src/middleware/validate-batch.test.ts b/api/src/middleware/validate-batch.test.ts index 21b04d0eba..eec034950a 100644 --- a/api/src/middleware/validate-batch.test.ts +++ b/api/src/middleware/validate-batch.test.ts @@ -1,17 +1,18 @@ -import type { NextFunction, Request, Response } from 'express'; +import type { Request, Response } from 'express'; import { validateBatch } from './validate-batch'; import '../../src/types/express.d.ts'; import { InvalidPayloadException } from '../exceptions'; import { FailedValidationException } from '@directus/shared/exceptions'; +import { vi, beforeEach, test, expect } from 'vitest'; let mockRequest: Partial; let mockResponse: Partial; -const nextFunction: NextFunction = jest.fn(); +const nextFunction = vi.fn(); beforeEach(() => { mockRequest = {}; mockResponse = {}; - jest.clearAllMocks(); + vi.clearAllMocks(); }); test('Sets body to empty, calls next on GET requests', async () => { @@ -39,7 +40,7 @@ test('Throws InvalidPayloadException on missing body', async () => { await validateBatch('read')(mockRequest as Request, mockResponse as Response, nextFunction); expect(nextFunction).toHaveBeenCalledTimes(1); - expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(InvalidPayloadException); + expect(vi.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(InvalidPayloadException); }); test(`Short circuits on Array body in update/delete use`, async () => { @@ -74,10 +75,10 @@ test(`Doesn't allow both query and keys in a batch delete`, async () => { query: { filter: {} }, }; - await validateBatch('delete')(mockRequest as Request, mockResponse as Response, nextFunction as NextFunction); + await validateBatch('delete')(mockRequest as Request, mockResponse as Response, nextFunction); expect(nextFunction).toHaveBeenCalledTimes(1); - expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(FailedValidationException); + expect(vi.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(FailedValidationException); }); test(`Requires 'data' on batch update`, async () => { @@ -87,10 +88,10 @@ test(`Requires 'data' on batch update`, async () => { query: { filter: {} }, }; - await validateBatch('update')(mockRequest as Request, mockResponse as Response, nextFunction as NextFunction); + await validateBatch('update')(mockRequest as Request, mockResponse as Response, nextFunction); expect(nextFunction).toHaveBeenCalledTimes(1); - expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(FailedValidationException); + expect(vi.mocked(nextFunction).mock.calls[0][0]).toBeInstanceOf(FailedValidationException); }); test(`Calls next when all is well`, async () => { @@ -101,8 +102,8 @@ test(`Calls next when all is well`, async () => { data: {}, }; - await validateBatch('update')(mockRequest as Request, mockResponse as Response, nextFunction as NextFunction); + await validateBatch('update')(mockRequest as Request, mockResponse as Response, nextFunction); expect(nextFunction).toHaveBeenCalledTimes(1); - expect(jest.mocked(nextFunction).mock.calls[0][0]).toBeUndefined(); + expect(vi.mocked(nextFunction).mock.calls[0][0]).toBeUndefined(); }); diff --git a/api/src/operations/exec/index.test.ts b/api/src/operations/exec/index.test.ts index fd70631060..3eadcaad3b 100644 --- a/api/src/operations/exec/index.test.ts +++ b/api/src/operations/exec/index.test.ts @@ -1,4 +1,5 @@ import { VMError } from 'vm2'; +import { test, expect } from 'vitest'; import config from './index'; @@ -29,7 +30,7 @@ test('Rejects when code contains syntax errors', async () => { FLOWS_EXEC_ALLOWED_MODULES: '', }, } as any) - ).rejects.toEqual(new Error("Couldn't compile code: Unexpected end of input")); + ).rejects.toEqual(new SyntaxError('Unexpected end of input')); }); test('Rejects when returned function does something illegal', async () => { diff --git a/api/src/services/fields.test.ts b/api/src/services/fields.test.ts index d8ea13d1a8..d1cd11c5a6 100644 --- a/api/src/services/fields.test.ts +++ b/api/src/services/fields.test.ts @@ -1,24 +1,25 @@ import { Field } from '@directus/shared/types'; import knex, { Knex } from 'knex'; import { getTracker, MockClient, Tracker } from 'knex-mock-client'; +import { afterEach, beforeAll, beforeEach, describe, expect, it, MockedFunction, SpyInstance, vi } from 'vitest'; import { FieldsService } from '.'; import { InvalidPayloadException } from '../exceptions'; -jest.mock('../../src/database/index', () => { +vi.mock('../../src/database/index', () => { return { __esModule: true, - default: jest.fn(), - getDatabaseClient: jest.fn().mockReturnValue('postgres'), - getSchemaInspector: jest.fn(), + default: vi.fn(), + getDatabaseClient: vi.fn().mockReturnValue('postgres'), + getSchemaInspector: vi.fn(), }; }); describe('Integration Tests', () => { - let db: jest.Mocked; + let db: MockedFunction; let tracker: Tracker; beforeAll(() => { - db = knex({ client: MockClient }) as jest.Mocked; + db = vi.mocked(knex({ client: MockClient })); tracker = getTracker(); }); @@ -36,11 +37,11 @@ describe('Integration Tests', () => { }); afterEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('addColumnToTable', () => { - let knexCreateTableBuilderSpy: jest.SpyInstance; + let knexCreateTableBuilderSpy: SpyInstance; it.each(['alias', 'unknown'])('%s fields should be skipped', async (type) => { const testCollection = 'test_collection'; @@ -101,7 +102,7 @@ describe('Integration Tests', () => { tracker.on.any(regex).response({}); await db.schema.alterTable(testCollection, (table) => { - knexCreateTableBuilderSpy = jest.spyOn(table, method as keyof Knex.CreateTableBuilder); + knexCreateTableBuilderSpy = vi.spyOn(table, method as keyof Knex.CreateTableBuilder); service.addColumnToTable(table, { collection: testCollection, @@ -129,7 +130,7 @@ describe('Integration Tests', () => { tracker.on.any(regex).response({}); await db.schema.alterTable(testCollection, (table) => { - knexCreateTableBuilderSpy = jest.spyOn(table, type as keyof Knex.CreateTableBuilder); + knexCreateTableBuilderSpy = vi.spyOn(table, type as keyof Knex.CreateTableBuilder); service.addColumnToTable(table, { collection: testCollection, @@ -158,7 +159,7 @@ describe('Integration Tests', () => { tracker.on.any(regex).response({}); await db.schema.alterTable(testCollection, (table) => { - knexCreateTableBuilderSpy = jest.spyOn(table, type as keyof Knex.CreateTableBuilder); + knexCreateTableBuilderSpy = vi.spyOn(table, type as keyof Knex.CreateTableBuilder); service.addColumnToTable(table, { collection: testCollection, @@ -187,7 +188,7 @@ describe('Integration Tests', () => { tracker.on.any(regex).response({}); await db.schema.alterTable('test_collection', (table) => { - knexCreateTableBuilderSpy = jest.spyOn(table, type as keyof Knex.CreateTableBuilder); + knexCreateTableBuilderSpy = vi.spyOn(table, type as keyof Knex.CreateTableBuilder); service.addColumnToTable(table, { collection: testCollection, @@ -217,7 +218,7 @@ describe('Integration Tests', () => { tracker.on.any(regex).response({}); await db.schema.alterTable(testCollection, (table) => { - knexCreateTableBuilderSpy = jest.spyOn(table, 'string'); + knexCreateTableBuilderSpy = vi.spyOn(table, 'string'); service.addColumnToTable(table, { collection: testCollection, @@ -244,7 +245,7 @@ describe('Integration Tests', () => { tracker.on.any(regex).response({}); await db.schema.alterTable(testCollection, (table) => { - knexCreateTableBuilderSpy = jest.spyOn(table, 'string'); + knexCreateTableBuilderSpy = vi.spyOn(table, 'string'); service.addColumnToTable(table, { collection: testCollection, @@ -273,7 +274,7 @@ describe('Integration Tests', () => { tracker.on.any(regex).response({}); await db.schema.alterTable(testCollection, (table) => { - knexCreateTableBuilderSpy = jest.spyOn(table, type as keyof Knex.CreateTableBuilder); + knexCreateTableBuilderSpy = vi.spyOn(table, type as keyof Knex.CreateTableBuilder); service.addColumnToTable(table, { collection: testCollection, @@ -298,7 +299,7 @@ describe('Integration Tests', () => { const regex = new RegExp(`alter table "${testCollection}" add column "${testField}" .*`); tracker.on.any(regex).response({}); - const thisHelpersStCreateColumnSpy = jest.spyOn(service.helpers.st, 'createColumn'); + const thisHelpersStCreateColumnSpy = vi.spyOn(service.helpers.st, 'createColumn'); await db.schema.alterTable(testCollection, (table) => { service.addColumnToTable(table, { @@ -334,7 +335,7 @@ describe('Integration Tests', () => { tracker.on.any(regex).response({}); await db.schema.alterTable(testCollection, (table) => { - knexCreateTableBuilderSpy = jest.spyOn(table, type as keyof Knex.CreateTableBuilder); + knexCreateTableBuilderSpy = vi.spyOn(table, type as keyof Knex.CreateTableBuilder); service.addColumnToTable(table, { collection: testCollection, diff --git a/api/src/services/files.test.ts b/api/src/services/files.test.ts index 15c42c7ba0..3c3f18d966 100644 --- a/api/src/services/files.test.ts +++ b/api/src/services/files.test.ts @@ -3,38 +3,39 @@ import knex, { Knex } from 'knex'; import { MockClient, Tracker, getTracker } from 'knex-mock-client'; import { FilesService, ItemsService } from '.'; import { InvalidPayloadException } from '../exceptions'; +import { describe, beforeAll, afterEach, expect, it, vi, beforeEach, SpyInstance } from 'vitest'; -jest.mock('exifr'); -jest.mock('../../src/database/index', () => { - return { getDatabaseClient: jest.fn().mockReturnValue('postgres') }; +vi.mock('exifr'); +vi.mock('../../src/database/index', () => { + return { getDatabaseClient: vi.fn().mockReturnValue('postgres') }; }); -jest.requireMock('../../src/database/index'); +vi.mock('../../src/database/index'); describe('Integration Tests', () => { - let db: jest.Mocked; + let db: Knex; let tracker: Tracker; beforeAll(async () => { - db = knex({ client: MockClient }) as jest.Mocked; + db = knex({ client: MockClient }); tracker = getTracker(); }); afterEach(() => { tracker.reset(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('Services / Files', () => { describe('createOne', () => { let service: FilesService; - let superCreateOne: jest.SpyInstance; + let superCreateOne: SpyInstance; beforeEach(() => { service = new FilesService({ knex: db, schema: { collections: {}, relations: [] }, }); - superCreateOne = jest.spyOn(ItemsService.prototype, 'createOne').mockImplementation(jest.fn()); + superCreateOne = vi.spyOn(ItemsService.prototype, 'createOne').mockReturnValue(Promise.resolve(1)); }); it('throws InvalidPayloadException when "type" is not provided', async () => { @@ -66,7 +67,7 @@ describe('Integration Tests', () => { describe('getMetadata', () => { let service: FilesService; - let exifrParseSpy: jest.SpyInstance; + let exifrParseSpy: SpyInstance; const sampleMetadata = { CustomTagA: 'value a', @@ -75,7 +76,7 @@ describe('Integration Tests', () => { }; beforeEach(() => { - exifrParseSpy = jest.spyOn(exifr, 'parse'); + exifrParseSpy = vi.spyOn(exifr, 'parse'); service = new FilesService({ knex: db, schema: { collections: {}, relations: [] }, diff --git a/api/src/services/items.test.ts b/api/src/services/items.test.ts index 829f32cb7b..b89f27dc06 100644 --- a/api/src/services/items.test.ts +++ b/api/src/services/items.test.ts @@ -5,14 +5,15 @@ import { ItemsService } from '../../src/services'; import { sqlFieldFormatter, sqlFieldList } from '../__utils__/items-utils'; import { systemSchema, userSchema } from '../__utils__/schemas'; import { cloneDeep } from 'lodash'; +import { describe, beforeAll, afterEach, it, expect, vi } from 'vitest'; -jest.mock('../../src/database/index', () => { - return { getDatabaseClient: jest.fn().mockReturnValue('postgres') }; +vi.mock('../../src/database/index', () => { + return { getDatabaseClient: vi.fn().mockReturnValue('postgres') }; }); -jest.requireMock('../../src/database/index'); +vi.mock('../../src/database/index'); describe('Integration Tests', () => { - let db: jest.Mocked; + let db: Knex; let tracker: Tracker; const schemas: Record = { @@ -21,7 +22,7 @@ describe('Integration Tests', () => { }; beforeAll(async () => { - db = knex({ client: MockClient }) as jest.Mocked; + db = knex({ client: MockClient }); tracker = getTracker(); }); diff --git a/api/src/services/notifications.test.ts b/api/src/services/notifications.test.ts index 160594474e..04886c8c4b 100644 --- a/api/src/services/notifications.test.ts +++ b/api/src/services/notifications.test.ts @@ -1,12 +1,13 @@ -import { NotificationsService, ItemsService } from '.'; +import { afterEach, beforeEach, describe, expect, it, SpyInstance, vi } from 'vitest'; +import { ItemsService, NotificationsService } from '.'; -jest.mock('../../src/env', () => ({ - ...jest.requireActual('../../src/env').default, +vi.mock('../../src/env', async () => ({ + ...(await vi.importActual('../../src/env')), PUBLIC_URL: '/', })); -jest.mock('../../src/database/index', () => { - return { __esModule: true, default: jest.fn(), getDatabaseClient: jest.fn().mockReturnValue('postgres') }; +vi.mock('../../src/database/index', () => { + return { __esModule: true, default: vi.fn(), getDatabaseClient: vi.fn().mockReturnValue('postgres') }; }); describe('Integration Tests', () => { @@ -20,16 +21,16 @@ describe('Integration Tests', () => { }); afterEach(() => { - jest.restoreAllMocks(); + vi.restoreAllMocks(); }); describe('createOne', () => { - let superCreateOneSpy: jest.SpyInstance; - let thisSendEmailSpy: jest.SpyInstance; + let superCreateOneSpy: SpyInstance; + let thisSendEmailSpy: SpyInstance; beforeEach(() => { - superCreateOneSpy = jest.spyOn(ItemsService.prototype, 'createOne').mockImplementation(jest.fn()); - thisSendEmailSpy = jest.spyOn(NotificationsService.prototype, 'sendEmail').mockImplementation(jest.fn()); + superCreateOneSpy = vi.spyOn(ItemsService.prototype, 'createOne').mockResolvedValue(0); + thisSendEmailSpy = vi.spyOn(NotificationsService.prototype, 'sendEmail').mockResolvedValue(); }); it('create a notification and send email', async () => { @@ -49,12 +50,12 @@ describe('Integration Tests', () => { }); describe('createMany', () => { - let superCreateManySpy: jest.SpyInstance; - let thisSendEmailSpy: jest.SpyInstance; + let superCreateManySpy: SpyInstance; + let thisSendEmailSpy: SpyInstance; beforeEach(() => { - superCreateManySpy = jest.spyOn(ItemsService.prototype, 'createMany').mockImplementation(jest.fn()); - thisSendEmailSpy = jest.spyOn(NotificationsService.prototype, 'sendEmail').mockImplementation(jest.fn()); + superCreateManySpy = vi.spyOn(ItemsService.prototype, 'createMany').mockResolvedValue([]); + thisSendEmailSpy = vi.spyOn(NotificationsService.prototype, 'sendEmail').mockResolvedValue(); }); it('create many notifications and send email for notification', async () => { @@ -81,12 +82,12 @@ describe('Integration Tests', () => { }); describe('sendEmail', () => { - let usersServiceReadOneSpy: jest.SpyInstance; - let mailServiceSendSpy: jest.SpyInstance; + let usersServiceReadOneSpy: SpyInstance; + let mailServiceSendSpy: SpyInstance; beforeEach(() => { - usersServiceReadOneSpy = jest.spyOn(service.usersService, 'readOne').mockImplementation(jest.fn()); - mailServiceSendSpy = jest.spyOn(service.mailService, 'send').mockImplementation(jest.fn()); + usersServiceReadOneSpy = vi.spyOn(service.usersService, 'readOne').mockResolvedValue({}); + mailServiceSendSpy = vi.spyOn(service.mailService, 'send').mockResolvedValue(0); }); it('do nothing when there is no recipient', async () => { diff --git a/api/src/services/payload.test.ts b/api/src/services/payload.test.ts index 9ed6d1c6c9..6648a0654c 100644 --- a/api/src/services/payload.test.ts +++ b/api/src/services/payload.test.ts @@ -2,18 +2,19 @@ import knex, { Knex } from 'knex'; import { MockClient, Tracker, getTracker } from 'knex-mock-client'; import { PayloadService } from '../../src/services'; import { getHelpers, Helpers } from '../../src/database/helpers'; +import { describe, beforeAll, afterEach, it, expect, vi, beforeEach } from 'vitest'; -jest.mock('../../src/database/index', () => { - return { getDatabaseClient: jest.fn().mockReturnValue('postgres') }; +vi.mock('../../src/database/index', () => { + return { getDatabaseClient: vi.fn().mockReturnValue('postgres') }; }); -jest.requireMock('../../src/database/index'); +vi.mock('../../src/database/index'); describe('Integration Tests', () => { - let db: jest.Mocked; + let db: Knex; let tracker: Tracker; beforeAll(async () => { - db = knex({ client: MockClient }) as jest.Mocked; + db = knex({ client: MockClient }); tracker = getTracker(); }); @@ -218,7 +219,6 @@ describe('Integration Tests', () => { ], 'read' ); - expect(result).toMatchObject([ { [dateFieldId]: '2022-01-10', diff --git a/api/src/services/specifications.test.ts b/api/src/services/specifications.test.ts index eadf70ae53..267fea6da3 100644 --- a/api/src/services/specifications.test.ts +++ b/api/src/services/specifications.test.ts @@ -1,26 +1,31 @@ import knex, { Knex } from 'knex'; import { getTracker, MockClient, Tracker } from 'knex-mock-client'; import { CollectionsService, FieldsService, RelationsService, SpecificationService } from '../../src/services'; +import { describe, beforeAll, afterEach, it, expect, vi, beforeEach } from 'vitest'; -jest.mock('../../src/database/index', () => { - return { getDatabaseClient: jest.fn().mockReturnValue('postgres') }; +vi.mock('../../src/database/index', async () => { + const actual = await vi.importActual('@directus/shared/utils/node'); + + return { + ...(actual as object), + getDatabaseClient: vi.fn().mockReturnValue('postgres'), + }; }); -jest.requireMock('../../src/database/index'); class Client_PG extends MockClient {} describe('Integration Tests', () => { - let db: jest.Mocked; + let db: Knex; let tracker: Tracker; beforeAll(async () => { - db = knex({ client: Client_PG }) as jest.Mocked; + db = knex({ client: Client_PG }); tracker = getTracker(); }); afterEach(() => { tracker.reset(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('Services / Specifications', () => { @@ -38,8 +43,8 @@ describe('Integration Tests', () => { describe('schema', () => { it('returns untyped schema for json fields', async () => { - jest.spyOn(CollectionsService.prototype, 'readByQuery').mockImplementation( - jest.fn().mockReturnValue([ + vi.spyOn(CollectionsService.prototype, 'readByQuery').mockImplementation( + vi.fn().mockReturnValue([ { collection: 'test_table', meta: { @@ -60,8 +65,8 @@ describe('Integration Tests', () => { ]) ); - jest.spyOn(FieldsService.prototype, 'readAll').mockImplementation( - jest.fn().mockReturnValue([ + vi.spyOn(FieldsService.prototype, 'readAll').mockImplementation( + vi.fn().mockReturnValue([ { collection: 'test_table', field: 'id', @@ -80,7 +85,7 @@ describe('Integration Tests', () => { }, ]) ); - jest.spyOn(RelationsService.prototype, 'readAll').mockImplementation(jest.fn().mockReturnValue([])); + vi.spyOn(RelationsService.prototype, 'readAll').mockImplementation(vi.fn().mockReturnValue([])); const spec = await service.oas.generate(); expect(spec.components?.schemas).toEqual({ @@ -121,12 +126,12 @@ describe('Integration Tests', () => { }, }; - jest - .spyOn(CollectionsService.prototype, 'readByQuery') - .mockImplementation(jest.fn().mockReturnValue([collection])); + vi.spyOn(CollectionsService.prototype, 'readByQuery').mockImplementation( + vi.fn().mockReturnValue([collection]) + ); - jest.spyOn(FieldsService.prototype, 'readAll').mockImplementation( - jest.fn().mockReturnValue([ + vi.spyOn(FieldsService.prototype, 'readAll').mockImplementation( + vi.fn().mockReturnValue([ { collection: collection.collection, field: 'id', @@ -137,7 +142,7 @@ describe('Integration Tests', () => { }, ]) ); - jest.spyOn(RelationsService.prototype, 'readAll').mockImplementation(jest.fn().mockReturnValue([])); + vi.spyOn(RelationsService.prototype, 'readAll').mockImplementation(vi.fn().mockReturnValue([])); const spec = await service.oas.generate(); diff --git a/api/src/services/users.test.ts b/api/src/services/users.test.ts index e6ef93e36d..f6ea11fe79 100644 --- a/api/src/services/users.test.ts +++ b/api/src/services/users.test.ts @@ -1,11 +1,12 @@ import { SchemaOverview } from '@directus/shared/types'; import knex, { Knex } from 'knex'; import { getTracker, MockClient, Tracker } from 'knex-mock-client'; -import { UsersService, ItemsService } from '.'; +import { afterEach, beforeAll, describe, it, vi, expect, MockedFunction } from 'vitest'; +import { ItemsService, UsersService } from '.'; import { InvalidPayloadException } from '../exceptions'; -jest.mock('../../src/database/index', () => { - return { __esModule: true, default: jest.fn(), getDatabaseClient: jest.fn().mockReturnValue('postgres') }; +vi.mock('../../src/database/index', () => { + return { __esModule: true, default: vi.fn(), getDatabaseClient: vi.fn().mockReturnValue('postgres') }; }); const testSchema = { @@ -39,11 +40,11 @@ const testSchema = { } as SchemaOverview; describe('Integration Tests', () => { - let db: jest.Mocked; + let db: MockedFunction; let tracker: Tracker; beforeAll(async () => { - db = knex({ client: MockClient }) as jest.Mocked; + db = vi.mocked(knex({ client: MockClient })); tracker = getTracker(); }); @@ -162,7 +163,7 @@ describe('Integration Tests', () => { accountability: { role: 'test', admin: false }, }); - jest.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(jest.fn(() => Promise.resolve([1]))); + vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(vi.fn(() => Promise.resolve([1]))); const promise = service.updateByQuery({}, { [field]: 'test' }); @@ -184,7 +185,7 @@ describe('Integration Tests', () => { accountability: { role: 'admin', admin: true }, }); - jest.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(jest.fn(() => Promise.resolve([1]))); + vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(vi.fn(() => Promise.resolve([1]))); const promise = service.updateByQuery({}, { [field]: 'test' }); @@ -199,7 +200,7 @@ describe('Integration Tests', () => { schema: testSchema, }); - jest.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(jest.fn(() => Promise.resolve([1]))); + vi.spyOn(ItemsService.prototype, 'getKeysByQuery').mockImplementation(vi.fn(() => Promise.resolve([1]))); const promise = service.updateByQuery({}, { [field]: 'test' }); diff --git a/api/src/utils/apply-snapshot.test.ts b/api/src/utils/apply-snapshot.test.ts index 0204655137..682f5f06aa 100644 --- a/api/src/utils/apply-snapshot.test.ts +++ b/api/src/utils/apply-snapshot.test.ts @@ -12,28 +12,29 @@ import { snapshotBeforeDeleteCollection, } from '../__utils__/snapshots'; import { Snapshot } from '../types'; +import { describe, afterEach, it, expect, vi, beforeEach } from 'vitest'; -jest.mock('../../src/database/index', () => { +vi.mock('../../src/database/index', () => { return { - getDatabaseClient: jest.fn().mockReturnValue('postgres'), + getDatabaseClient: vi.fn().mockReturnValue('postgres'), }; }); -jest.requireMock('../../src/database/index'); +vi.mock('../../src/database/index'); class Client_PG extends MockClient {} describe('applySnapshot', () => { - let db: jest.Mocked; + let db: Knex; let tracker: Tracker; beforeEach(() => { - db = knex({ client: Client_PG }) as jest.Mocked; + db = knex({ client: Client_PG }); tracker = getTracker(); }); afterEach(() => { tracker.reset(); - jest.clearAllMocks(); + vi.clearAllMocks(); }); describe('Creating new collection(s)', () => { @@ -102,12 +103,14 @@ describe('applySnapshot', () => { }; // Stop call to db later on in apply-snapshot - jest.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema)); + vi.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema)); // We are not actually testing that createOne works, just that is is called correctly - const createOneCollectionSpy = jest + const createOneCollectionSpy = vi .spyOn(CollectionsService.prototype, 'createOne') - .mockImplementation(jest.fn()); - const createFieldSpy = jest.spyOn(FieldsService.prototype, 'createField').mockImplementation(jest.fn()); + .mockImplementation(vi.fn().mockReturnValue([])); + const createFieldSpy = vi + .spyOn(FieldsService.prototype, 'createField') + .mockImplementation(vi.fn().mockReturnValue([])); await applySnapshot(snapshotCreateCollectionNotNested, { database: db, @@ -251,12 +254,14 @@ describe('applySnapshot', () => { }; // Stop call to db later on in apply-snapshot - jest.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema)); + vi.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema)); // We are not actually testing that createOne works, just that is is called correctly - const createOneCollectionSpy = jest + const createOneCollectionSpy = vi .spyOn(CollectionsService.prototype, 'createOne') - .mockImplementation(jest.fn()); - const createFieldSpy = jest.spyOn(FieldsService.prototype, 'createField').mockImplementation(jest.fn()); + .mockImplementation(vi.fn().mockReturnValue([])); + const createFieldSpy = vi + .spyOn(FieldsService.prototype, 'createField') + .mockImplementation(vi.fn().mockReturnValue([])); await applySnapshot(snapshotCreateCollection, { database: db, @@ -285,11 +290,11 @@ describe('applySnapshot', () => { }; // Stop call to db later on in apply-snapshot - jest.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema)); + vi.spyOn(getSchema, 'getSchema').mockReturnValue(Promise.resolve(snapshotApplyTestSchema)); // We are not actually testing that deleteOne works, just that is is called correctly - const deleteOneCollectionSpy = jest + const deleteOneCollectionSpy = vi .spyOn(CollectionsService.prototype, 'deleteOne') - .mockImplementation(jest.fn()); + .mockImplementation(vi.fn().mockReturnValue([])); await applySnapshot(snapshotToApply, { database: db, diff --git a/api/src/utils/async-handler.test.ts b/api/src/utils/async-handler.test.ts index 8d200aae5f..95d40dfd69 100644 --- a/api/src/utils/async-handler.test.ts +++ b/api/src/utils/async-handler.test.ts @@ -1,10 +1,11 @@ -import type { RequestHandler, NextFunction, Request, Response } from 'express'; +import type { RequestHandler, Request, Response } from 'express'; import '../../src/types/express.d.ts'; import asyncHandler from './async-handler'; +import { expect, vi, test } from 'vitest'; let mockRequest: Partial; let mockResponse: Partial; -const nextFunction: NextFunction = jest.fn(); +const nextFunction = vi.fn(); test('Wraps async middleware in Promise resolve that will catch rejects and pass them to the nextFn', async () => { const err = new Error('testing'); @@ -13,7 +14,7 @@ test('Wraps async middleware in Promise resolve that will catch rejects and pass throw err; }; - await asyncHandler(middleware)(mockRequest as Request, mockResponse as Response, nextFunction as NextFunction); + await asyncHandler(middleware)(mockRequest as Request, mockResponse as Response, nextFunction); expect(nextFunction).toHaveBeenCalledWith(err); }); diff --git a/api/src/utils/calculate-field-depth.test.ts b/api/src/utils/calculate-field-depth.test.ts index 65dcd9ef83..e66f5ff82c 100644 --- a/api/src/utils/calculate-field-depth.test.ts +++ b/api/src/utils/calculate-field-depth.test.ts @@ -1,4 +1,5 @@ import { calculateFieldDepth } from '../../src/utils/calculate-field-depth'; +import { test, expect } from 'vitest'; test('Calculates basic depth', () => { const filter = { diff --git a/api/src/utils/filter-items.test.ts b/api/src/utils/filter-items.test.ts index cab59b74d1..d78c8097ec 100644 --- a/api/src/utils/filter-items.test.ts +++ b/api/src/utils/filter-items.test.ts @@ -1,4 +1,5 @@ import { filterItems } from '../../src/utils/filter-items'; +import { describe, test, expect } from 'vitest'; const items = [ { diff --git a/api/src/utils/get-auth-providers.test.ts b/api/src/utils/get-auth-providers.test.ts index 586643aff5..f294ce059c 100644 --- a/api/src/utils/get-auth-providers.test.ts +++ b/api/src/utils/get-auth-providers.test.ts @@ -1,19 +1,18 @@ +import { describe, expect, vi, test } from 'vitest'; +import { getAuthProviders } from '../../src/utils/get-auth-providers'; + let factoryEnv: { [k: string]: any } = {}; -jest.mock( - '../../src/env', - () => - new Proxy( - {}, - { - get(target, prop) { - return factoryEnv[prop as string]; - }, - } - ) -); - -import { getAuthProviders } from '../../src/utils/get-auth-providers'; +vi.mock('../../src/env', () => ({ + default: new Proxy( + {}, + { + get(_target, prop) { + return factoryEnv[prop as string]; + }, + } + ), +})); const scenarios = [ { diff --git a/api/src/utils/get-cache-key.test.ts b/api/src/utils/get-cache-key.test.ts index 9b24550a17..d0e9084908 100644 --- a/api/src/utils/get-cache-key.test.ts +++ b/api/src/utils/get-cache-key.test.ts @@ -1,5 +1,6 @@ import { Request } from 'express'; import { getCacheKey } from '../../src/utils/get-cache-key'; +import { describe, test, expect } from 'vitest'; const restUrl = 'http://localhost/items/example'; const graphQlUrl = 'http://localhost/graphql'; diff --git a/api/src/utils/get-column-path.test.ts b/api/src/utils/get-column-path.test.ts index d71292202c..8e61b0e1f5 100644 --- a/api/src/utils/get-column-path.test.ts +++ b/api/src/utils/get-column-path.test.ts @@ -1,6 +1,7 @@ import { getColumnPath, ColPathProps } from '../../src/utils/get-column-path'; import { InvalidQueryException } from '../../src/exceptions'; import { DeepPartial } from '@directus/shared/types'; +import { test, expect } from 'vitest'; /* { diff --git a/api/src/utils/get-config-from-env.test.ts b/api/src/utils/get-config-from-env.test.ts index f37100afe1..930ab24170 100644 --- a/api/src/utils/get-config-from-env.test.ts +++ b/api/src/utils/get-config-from-env.test.ts @@ -1,10 +1,13 @@ import { getConfigFromEnv } from '../../src/utils/get-config-from-env'; +import { describe, test, expect, vi } from 'vitest'; -jest.mock('../../src/env', () => ({ - OBJECT_BRAND__COLOR: 'purple', - OBJECT_BRAND__HEX: '#6644FF', - CAMELCASE_OBJECT__FIRST_KEY: 'firstValue', - CAMELCASE_OBJECT__SECOND_KEY: 'secondValue', +vi.mock('../../src/env', () => ({ + default: { + OBJECT_BRAND__COLOR: 'purple', + OBJECT_BRAND__HEX: '#6644FF', + CAMELCASE_OBJECT__FIRST_KEY: 'firstValue', + CAMELCASE_OBJECT__SECOND_KEY: 'secondValue', + }, })); describe('get config from env', () => { diff --git a/api/src/utils/get-relation-info.test.ts b/api/src/utils/get-relation-info.test.ts index 6a7a71b7fe..1662b6488c 100644 --- a/api/src/utils/get-relation-info.test.ts +++ b/api/src/utils/get-relation-info.test.ts @@ -1,5 +1,6 @@ import { getRelationInfo } from '../../src/utils/get-relation-info'; import { Relation, DeepPartial } from '@directus/shared/types'; +import { describe, expect, it } from 'vitest'; describe('getRelationInfo', () => { it('Errors on suspiciously long implicit $FOLLOW', () => { diff --git a/api/src/utils/get-relation-type.test.ts b/api/src/utils/get-relation-type.test.ts index 451bac11b4..b2c85f49fb 100644 --- a/api/src/utils/get-relation-type.test.ts +++ b/api/src/utils/get-relation-type.test.ts @@ -1,5 +1,6 @@ import { getRelationType } from '../../src/utils/get-relation-type'; import { Relation } from '@directus/shared/types'; +import { test, expect } from 'vitest'; test('Returns null if no relation object is included', () => { const result = getRelationType({ relation: null, collection: null, field: 'test' }); diff --git a/api/src/utils/get-string-byte-size.test.ts b/api/src/utils/get-string-byte-size.test.ts index 2b6469ceff..6ca5121063 100644 --- a/api/src/utils/get-string-byte-size.test.ts +++ b/api/src/utils/get-string-byte-size.test.ts @@ -1,4 +1,5 @@ import { stringByteSize } from '../../src/utils/get-string-byte-size'; +import { test, expect } from 'vitest'; test('Returns correct byte size for given input string', () => { expect(stringByteSize('test')).toBe(4); diff --git a/api/src/utils/is-directus-jwt.test.ts b/api/src/utils/is-directus-jwt.test.ts index 682bffc2c3..73a33f8a4e 100644 --- a/api/src/utils/is-directus-jwt.test.ts +++ b/api/src/utils/is-directus-jwt.test.ts @@ -1,5 +1,6 @@ import isDirectusJWT from '../../src/utils/is-directus-jwt'; import jwt from 'jsonwebtoken'; +import { test, expect } from 'vitest'; test('Returns false for non JWT string', () => { const result = isDirectusJWT('test'); diff --git a/api/src/utils/jwt.test.ts b/api/src/utils/jwt.test.ts index 1296497c85..3068bd36bf 100644 --- a/api/src/utils/jwt.test.ts +++ b/api/src/utils/jwt.test.ts @@ -2,6 +2,7 @@ import { verifyAccessJWT } from '../../src/utils/jwt'; import jwt from 'jsonwebtoken'; import { InvalidTokenException, ServiceUnavailableException, TokenExpiredException } from '../../src/exceptions'; import { DirectusTokenPayload } from '../../src/types'; +import { test, expect, vi } from 'vitest'; const payload: DirectusTokenPayload = { role: null, app_access: false, admin_access: false }; const secret = 'test-secret'; @@ -32,7 +33,7 @@ Object.entries(InvalidTokenCases).forEach(([title, token]) => ); test(`Throws ServiceUnavailableException for unexpected error from jsonwebtoken`, () => { - jest.spyOn(jwt, 'verify').mockImplementation(() => { + vi.spyOn(jwt, 'verify').mockImplementation(() => { throw new Error(); }); diff --git a/api/src/utils/merge-permissions.test.ts b/api/src/utils/merge-permissions.test.ts index dd8a5718dc..df4e6500c9 100644 --- a/api/src/utils/merge-permissions.test.ts +++ b/api/src/utils/merge-permissions.test.ts @@ -1,5 +1,6 @@ import { mergePermission } from '../../src/utils/merge-permissions'; import { Permission, Filter } from '@directus/shared/types'; +import { describe, expect, test } from 'vitest'; const fullFilter = {} as Filter; const conditionalFilter = { user: { id: { _eq: '$CURRENT_USER' } } } as Filter; diff --git a/api/src/utils/validate-keys.test.ts b/api/src/utils/validate-keys.test.ts index e6ebb5ae13..0ca21ce72a 100644 --- a/api/src/utils/validate-keys.test.ts +++ b/api/src/utils/validate-keys.test.ts @@ -1,6 +1,7 @@ import { validateKeys } from '../../src/utils/validate-keys'; import { SchemaOverview } from '@directus/shared/types'; import { v4 as uuid } from 'uuid'; +import { describe, expect, it } from 'vitest'; const schema: SchemaOverview = { collections: { diff --git a/api/tsconfig.json b/api/tsconfig.json index 937fadee51..c635555e40 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -11,8 +11,7 @@ "lib": ["es2019"], "skipLibCheck": true, "declaration": true, - "resolveJsonModule": true, - "types": ["jest"] + "resolveJsonModule": true }, - "exclude": ["node_modules", "dist", "extensions", "tests"] + "exclude": ["node_modules", "dist", "extensions", "**/__utils__/*.*", "**/__mocks__/*.*", "**/*.test.ts"] } diff --git a/api/vitest.config.js b/api/vitest.config.js new file mode 100644 index 0000000000..03728ac2e2 --- /dev/null +++ b/api/vitest.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + test: { + globalSetup: 'globalSetup.js', + alias: [ + // TODO: Remove this after moving to ESM + { + find: '@directus/format-title', + replacement: path.resolve(__dirname, '../app/node_modules/@directus/format-title'), + }, + ], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc80f375e9..dcb90974f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -99,7 +99,6 @@ importers: '@types/flat': 5.0.2 '@types/fs-extra': 9.0.13 '@types/inquirer': 8.2.1 - '@types/jest': 29.2.0 '@types/js-yaml': 4.0.5 '@types/json2csv': 5.0.3 '@types/jsonwebtoken': 8.5.9 @@ -123,6 +122,7 @@ importers: '@types/uuid': 8.3.4 '@types/uuid-validate': 0.0.1 '@types/wellknown': 0.5.3 + '@vitest/coverage-c8': ^0.25.1 argon2: 0.30.1 async: 3.2.4 async-mutex: 0.4.0 @@ -158,7 +158,6 @@ importers: helmet: 6.0.0 inquirer: 8.2.4 ioredis: 5.2.3 - jest: 29.2.1 joi: 17.6.3 js-yaml: 4.1.0 js2xmlparser: 5.0.0 @@ -208,13 +207,13 @@ importers: supertest: 6.3.0 tedious: 15.1.0 tmp-promise: 3.0.3 - ts-jest: 29.0.3 ts-node: 10.9.1 ts-node-dev: 2.0.0 typescript: 4.8.4 update-check: 1.5.4 uuid: 9.0.0 uuid-validate: 0.0.3 + vitest: 0.25.1 vm2: 3.9.11 wellknown: 0.5.0 dependencies: @@ -336,7 +335,6 @@ importers: '@types/flat': 5.0.2 '@types/fs-extra': 9.0.13 '@types/inquirer': 8.2.1 - '@types/jest': 29.2.0 '@types/js-yaml': 4.0.5 '@types/json2csv': 5.0.3 '@types/jsonwebtoken': 8.5.9 @@ -360,17 +358,17 @@ importers: '@types/uuid': 8.3.4 '@types/uuid-validate': 0.0.1 '@types/wellknown': 0.5.3 + '@vitest/coverage-c8': 0.25.2 copyfiles: 2.4.1 cross-env: 7.0.3 form-data: 4.0.0 - jest: 29.2.1_5uyhgycj63wuqgvl4exdnr442q knex-mock-client: 1.8.4_knex@2.3.0 rimraf: 3.0.2 supertest: 6.3.0 - ts-jest: 29.0.3_7yfpbkrrkkmtlepb2un4d37cti ts-node: 10.9.1_id5sxmpllzol2kp2zgqrnepaum ts-node-dev: 2.0.0_id5sxmpllzol2kp2zgqrnepaum typescript: 4.8.4 + vitest: 0.25.1 app: specifiers: @@ -3611,48 +3609,6 @@ packages: - ts-node dev: true - /@jest/core/29.2.1_ts-node@10.9.1: - resolution: {integrity: sha512-kuLKYqnqgerXkBUwlHVxeSuhSnd+JMnMCLfU98bpacBSfWEJPegytDh3P2m15/JHzet32hGGld4KR4OzMb6/Tg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/console': 29.2.1 - '@jest/reporters': 29.2.1 - '@jest/test-result': 29.2.1 - '@jest/transform': 29.2.1 - '@jest/types': 29.2.1 - '@types/node': 18.11.2 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.3.2 - exit: 0.1.2 - graceful-fs: 4.2.10 - jest-changed-files: 29.2.0 - jest-config: 29.2.1_5uyhgycj63wuqgvl4exdnr442q - jest-haste-map: 29.2.1 - jest-message-util: 29.2.1 - jest-regex-util: 29.2.0 - jest-resolve: 29.2.1 - jest-resolve-dependencies: 29.2.1 - jest-runner: 29.2.1 - jest-runtime: 29.2.1 - jest-snapshot: 29.2.1 - jest-util: 29.2.1 - jest-validate: 29.2.1 - jest-watcher: 29.2.1 - micromatch: 4.0.5 - pretty-format: 29.2.1 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - supports-color - - ts-node - dev: true - /@jest/environment/29.2.1: resolution: {integrity: sha512-EutqA7T/X6zFjw6mAWRHND+ZkTPklmIEWCNbmwX6uCmOrFrWaLbDZjA+gePHJx6fFMMRvNfjXcvzXEtz54KPlg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7062,6 +7018,24 @@ packages: vue: 3.2.41 dev: true + /@vitest/coverage-c8/0.25.2: + resolution: {integrity: sha512-qKsiUJh3bjbB5Q229CbxEWCqiDBwvIrcZ9OOuQdMEC0pce3/LlTUK3+K3hd7WqAYEbbiqXfC5MVMKHZkV82PgA==} + dependencies: + c8: 7.12.0 + vitest: 0.25.2 + transitivePeerDependencies: + - '@edge-runtime/vm' + - '@vitest/browser' + - '@vitest/ui' + - happy-dom + - jsdom + - less + - sass + - stylus + - supports-color + - terser + dev: true + /@vue/compiler-core/3.2.41: resolution: {integrity: sha512-oA4mH6SA78DT+96/nsi4p9DX97PHcNROxs51lYk7gb9Z4BPKQ3Mh+BLn6CQZBw857Iuhu28BfMSRHAlPvD4vlw==} dependencies: @@ -13266,34 +13240,6 @@ packages: - ts-node dev: true - /jest-cli/29.2.1_5uyhgycj63wuqgvl4exdnr442q: - resolution: {integrity: sha512-UIMD5aNqvPKpdlJSaeUAoLfxsh9TZvOkaMETx5qXnkboc317bcbb0eLHbIj8sFBHdcJAIAM+IRKnIU7Wi61MBw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.2.1_ts-node@10.9.1 - '@jest/test-result': 29.2.1 - '@jest/types': 29.2.1 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.10 - import-local: 3.1.0 - jest-config: 29.2.1_5uyhgycj63wuqgvl4exdnr442q - jest-util: 29.2.1 - jest-validate: 29.2.1 - prompts: 2.4.2 - yargs: 17.5.1 - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /jest-cli/29.2.1_@types+node@18.11.2: resolution: {integrity: sha512-UIMD5aNqvPKpdlJSaeUAoLfxsh9TZvOkaMETx5qXnkboc317bcbb0eLHbIj8sFBHdcJAIAM+IRKnIU7Wi61MBw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -13360,46 +13306,6 @@ packages: - supports-color dev: true - /jest-config/29.2.1_5uyhgycj63wuqgvl4exdnr442q: - resolution: {integrity: sha512-EV5F1tQYW/quZV2br2o88hnYEeRzG53Dfi6rSG3TZBuzGQ6luhQBux/RLlU5QrJjCdq3LXxRRM8F1LP6DN1ycA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - dependencies: - '@babel/core': 7.19.3 - '@jest/test-sequencer': 29.2.1 - '@jest/types': 29.2.1 - '@types/node': 18.11.2 - babel-jest: 29.2.1_@babel+core@7.19.3 - chalk: 4.1.2 - ci-info: 3.3.2 - deepmerge: 4.2.2 - glob: 7.2.3 - graceful-fs: 4.2.10 - jest-circus: 29.2.1 - jest-environment-node: 29.2.1 - jest-get-type: 29.2.0 - jest-regex-util: 29.2.0 - jest-resolve: 29.2.1 - jest-runner: 29.2.1 - jest-util: 29.2.1 - jest-validate: 29.2.1 - micromatch: 4.0.5 - parse-json: 5.2.0 - pretty-format: 29.2.1 - slash: 3.0.0 - strip-json-comments: 3.1.1 - ts-node: 10.9.1_id5sxmpllzol2kp2zgqrnepaum - transitivePeerDependencies: - - supports-color - dev: true - /jest-config/29.2.1_@types+node@18.11.2: resolution: {integrity: sha512-EV5F1tQYW/quZV2br2o88hnYEeRzG53Dfi6rSG3TZBuzGQ6luhQBux/RLlU5QrJjCdq3LXxRRM8F1LP6DN1ycA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -13812,26 +13718,6 @@ packages: - ts-node dev: true - /jest/29.2.1_5uyhgycj63wuqgvl4exdnr442q: - resolution: {integrity: sha512-K0N+7rx+fv3Us3KhuwRSJt55MMpZPs9Q3WSO/spRZSnsalX8yEYOTQ1PiSN7OvqzoRX4JEUXCbOJRlP4n8m5LA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.2.1_ts-node@10.9.1 - '@jest/types': 29.2.1 - import-local: 3.1.0 - jest-cli: 29.2.1_5uyhgycj63wuqgvl4exdnr442q - transitivePeerDependencies: - - '@types/node' - - supports-color - - ts-node - dev: true - /jest/29.2.1_@types+node@18.11.2: resolution: {integrity: sha512-K0N+7rx+fv3Us3KhuwRSJt55MMpZPs9Q3WSO/spRZSnsalX8yEYOTQ1PiSN7OvqzoRX4JEUXCbOJRlP4n8m5LA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -19935,7 +19821,7 @@ packages: dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.2.1_5uyhgycj63wuqgvl4exdnr442q + jest: 29.2.1_@types+node@18.11.2 jest-util: 29.2.1 json5: 2.2.1 lodash.memoize: 4.1.2 @@ -20704,6 +20590,94 @@ packages: - terser dev: true + /vitest/0.25.1: + resolution: {integrity: sha512-eH74h6MkuEgsqR4mAQZeMK9O0PROiKY+i+1GMz/fBi5A3L2ml5U7JQs7LfPU7+uWUziZyLHagl+rkyfR8SLhlA==} + engines: {node: '>=v14.16.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/chai': 4.3.3 + '@types/chai-subset': 1.3.3 + '@types/node': 18.11.2 + acorn: 8.8.0 + acorn-walk: 8.2.0 + chai: 4.3.6 + debug: 4.3.4 + local-pkg: 0.4.2 + source-map: 0.6.1 + strip-literal: 0.4.2 + tinybench: 2.3.1 + tinypool: 0.3.0 + tinyspy: 1.0.2 + vite: 3.1.8 + transitivePeerDependencies: + - less + - sass + - stylus + - supports-color + - terser + dev: true + + /vitest/0.25.2: + resolution: {integrity: sha512-qqkzfzglEFbQY7IGkgSJkdOhoqHjwAao/OrphnHboeYHC5JzsVFoLCaB2lnAy8krhj7sbrFTVRApzpkTOeuDWQ==} + engines: {node: '>=v14.16.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/chai': 4.3.3 + '@types/chai-subset': 1.3.3 + '@types/node': 18.11.2 + acorn: 8.8.0 + acorn-walk: 8.2.0 + chai: 4.3.6 + debug: 4.3.4 + local-pkg: 0.4.2 + source-map: 0.6.1 + strip-literal: 0.4.2 + tinybench: 2.3.1 + tinypool: 0.3.0 + tinyspy: 1.0.2 + vite: 3.1.8 + transitivePeerDependencies: + - less + - sass + - stylus + - supports-color + - terser + dev: true + /vm-browserify/1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} dev: true