New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@directus/api

Package Overview
Dependencies
Maintainers
4
Versions
105
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@directus/api

Directus is a real-time API and App dashboard for managing SQL database content

latest
Source
npmnpm
Version
35.0.1
Version published
Weekly downloads
20K
-22.1%
Maintainers
4
Weekly downloads
 
Created
Source

Service Test Mocks

Shared mocking utilities for service layer tests. These utilities help reduce code duplication and provide consistent mocking patterns across service tests.

Overview

This directory contains mock implementations for commonly used modules in service tests:

  • knex.ts - Knex instance, query tracker, and schema builder mocks
  • database.ts - Database client and transaction mocks
  • cache.ts - Cache system mocks
  • schema.ts - Schema inspector mocks
  • emitter.ts - Event emitter mocks
  • env.ts - env mocks
  • storage.ts - Storage driver and manager mocks
  • items-service.ts - ItemsService mocks
  • fields-service.ts - FieldsService mocks
  • files-service.ts - FilesService mocks
  • folders-service.ts - FoldersService mocks
  • test-helpers.ts - Test data factory functions
  • controllers.ts - Controller/router testing helpers

Quick Start

import { createMockKnex, resetKnexMocks } from '../test-utils/knex.js';

// Set up mocks
vi.mock('../../src/database/index', async () => {
	const { mockDatabase } = await import('../test-utils/database.js');
	return mockDatabase();
});

vi.mock('../cache.js', async () => {
	const { mockCache } = await import('../test-utils/cache.js');
	return mockCache();
});

describe('YourService Tests', () => {
	const { db, tracker, mockSchemaBuilder } = createMockKnex();

	afterEach(() => {
		resetKnexMocks(tracker, mockSchemaBuilder);
	});

	// Your tests here
});

File Documentation

knex.ts

Provides Knex mocking utilities including the database client, query tracker, and schema builder.

createMockKnex()

Creates a complete mocked Knex instance with tracker and schema builder support.

Returns: { db, tracker, mockSchemaBuilder }

  • db: Mocked Knex client (connected to knex-mock-client)
  • tracker: Query tracker for mocking database responses
  • mockSchemaBuilder: Mocked schema builder with createTable, dropTable, alterTable, etc.

Example:

const { db, tracker, mockSchemaBuilder } = createMockKnex();

// Mock query responses
tracker.on.select('users').response([{ id: 1, name: 'John' }]);
tracker.on.insert('users').response([1]);

// Use in service constructor
const service = new YourService({ knex: db, schema });

// Verify schema operations
expect(mockSchemaBuilder.createTable).toHaveBeenCalledWith('users', expect.any(Function));

createMockTableBuilder()

Creates a mock Knex table builder with all column types, modifiers, and constraints.

Returns: Mock table builder with chainable methods

Example:

const table = createMockTableBuilder();
table.string('name', 255).notNullable().index();
table.integer('age').unsigned().nullable();

setupSystemCollectionMocks(tracker)

Sets up default CRUD operation mocks for all Directus system collections (directus_collections, directus_fields, etc.).

Parameters:

  • tracker: The knex-mock-client tracker instance

Example:

const { tracker } = createMockKnex();
setupSystemCollectionMocks(tracker);
// Now all system collection queries return empty arrays by default

resetKnexMocks(tracker, mockSchemaBuilder)

Resets all mock states. Should be called in afterEach hooks.

Parameters:

  • tracker: The knex-mock-client tracker instance
  • mockSchemaBuilder: The mock schema builder object from createMockKnex

Example:

const { db, tracker, mockSchemaBuilder } = createMockKnex();

afterEach(() => {
	resetKnexMocks(tracker, mockSchemaBuilder);
});

mockCreateTable()

Creates a mock for db.schema.createTable that executes the callback with a mock table builder.

Returns: vi.fn() that can be assigned to db.schema.createTable

Example:

const { db } = createMockKnex();
const createTableSpy = mockCreateTable();
db.schema.createTable = createTableSpy as any;

await db.schema.createTable('users', (table) => {
	table.increments('id');
	table.string('name');
});

expect(createTableSpy).toHaveBeenCalledWith('users', expect.any(Function));

mockAlterTable()

Creates a mock for db.schema.alterTable that executes the callback with a mock table builder.

Returns: vi.fn() that can be assigned to db.schema.alterTable

Example:

const { db } = createMockKnex();
const alterTableSpy = mockAlterTable();
db.schema.alterTable = alterTableSpy as any;

await db.schema.alterTable('users', (table) => {
	table.string('email');
});

mockSchemaTable()

Creates a mock for db.schema.table that executes the callback with a mock table builder.

Returns: vi.fn() that can be assigned to db.schema.table

Example:

const { db } = createMockKnex();
const schemaTableSpy = mockSchemaTable();
db.schema.table = schemaTableSpy as any;

await db.schema.table('users', (table) => {
	table.dropColumn('old_field');
});

database.ts

Provides database module mocking utilities including database client detection and transaction handling.

mockDatabase(client?)

Creates a standard database module mock for service tests.

Parameters:

  • client (optional): Database client to mock (default: 'postgres')
    • Supported values: 'postgres', 'mysql', 'sqlite3', 'mssql', 'oracledb', 'cockroachdb'

Returns: Mock module object with getDatabaseClient and getSchemaInspector

Example:

// Standard PostgreSQL mock
vi.mock('../../src/database/index', async () => {
	const { mockDatabase } = await import('../test-utils/database.js');
	return mockDatabase();
});

// MySQL-specific mock
vi.mock('../../src/database/index', async () => {
	const { mockDatabase } = await import('../test-utils/database.js');
	return mockDatabase('mysql');
});

// Dynamically change client during tests
import { getDatabaseClient } from '../../src/database/index.js';
vi.mocked(getDatabaseClient).mockReturnValue('sqlite3');

mockTransaction()

Creates a mock for the transaction utility. By default, executes the callback with the provided Knex instance (no actual transaction wrapper).

Returns: Mock module object with transaction function

Example:

vi.mock('../utils/transaction.js', async () => {
	const { mockTransaction } = await import('../test-utils/database.js');
	return mockTransaction();
});

// Transaction callback is executed immediately with the same knex instance
await transaction(db, async (trx) => {
	// trx === db in tests
	await trx('users').insert({ name: 'John' });
});

cache.ts

Provides cache system mocking utilities.

mockCache()

Creates a standard cache module mock with getCache, getCacheValue, setCacheValue, and clearSystemCache. Returns both the mocks for vi.mock() declarations and spies for testing cache behavior.

Returns: Object with mock functions and spies

  • getCache: Mock function returning cache object
  • getCacheValue: Mock function returning null
  • setCacheValue: Mock function returning undefined
  • clearSystemCache: Mock function
  • spies: Spy functions for testing cache behavior
    • clearSpy: Spy for cache.clear()
    • systemClearSpy: Spy for systemCache.clear()
    • getCacheSpy: Spy for localSchemaCache.get()
    • setCacheSpy: Spy for localSchemaCache.set()
    • mockCacheReturn: The mock cache object to pass to vi.mocked()

Example:

// Standard usage for vi.mock()
vi.mock('../cache.js', async () => {
	const { mockCache } = await import('../test-utils/cache.js');
	return mockCache();
});

// Testing cache clearing with spies
import { getCache } from '../cache.js';
import { mockCache } from '../test-utils/cache.js';

test('should clear cache after update', async () => {
	const { spies } = mockCache();
	vi.mocked(getCache).mockReturnValue(spies.mockCacheReturn as any);

	const service = new YourService({ knex: db, schema });
	await service.updateOne('1', { name: 'Updated' });

	expect(spies.clearSpy).toHaveBeenCalled();
});

schema.ts

Provides schema inspector mocking utilities for the @directus/schema package.

mockSchema()

Creates a standard schema inspector mock with tableInfo, columnInfo, primary, foreignKeys, etc.

Returns: Mock module object for vi.mock()

Example:

// Standard usage
vi.mock('@directus/schema', async () => {
	const { mockSchema } = await import('../test-utils/schema.js');
	return mockSchema();
});

// Dynamically change inspector behavior during tests
import { createInspector } from '@directus/schema';
vi.mocked(createInspector).mockReturnValue({
	tableInfo: vi.fn().mockResolvedValue([{ name: 'users' }, { name: 'posts' }]),
	columnInfo: vi.fn().mockResolvedValue([
		{ table: 'users', name: 'id', data_type: 'integer', is_nullable: false },
		{ table: 'users', name: 'name', data_type: 'varchar', is_nullable: true },
	]),
	primary: vi.fn().mockResolvedValue('id'),
	foreignKeys: vi.fn().mockResolvedValue([]),
	withSchema: vi.fn().mockReturnThis(),
} as any);

emitter.ts

Provides event emitter mocking utilities.

mockEmitter()

Creates a standard emitter mock with emitAction, emitFilter, emitInit, and event listener methods.

Returns: Mock module object for vi.mock()

Example:

// Standard usage
vi.mock('../emitter.js', async () => {
	const { mockEmitter } = await import('../test-utils/emitter.js');
	return mockEmitter();
});

// Dynamically change emitter behavior during tests
import emitter from '../emitter.js';

// Mock filter to modify payload
vi.mocked(emitter.emitFilter).mockResolvedValue({ modified: true });

// Verify action was emitted
await service.createOne(data);
expect(emitter.emitAction).toHaveBeenCalledWith(
	'items.create',
	expect.objectContaining({ collection: 'test_collection' }),
	expect.any(Object),
);

env.ts

Provides environment variable mocking utilities for the @directus/env package.

mockEnv(overrides?)

Creates a standard environment mock with sensible test defaults for commonly used environment variables.

Parameters:

  • overrides (optional): Object containing environment variable overrides to merge with defaults

Returns: Mock module object with useEnv function

Default environment variables:

EXTENSIONS_PATH: './extensions',
STORAGE_LOCATIONS: 'local',
EMAIL_TEMPLATES_PATH: './templates'

Example:

// Dynamically changing env values during tests
import { useEnv } from '@directus/env';
const { resetEnvMock } = await import('../test-utils/env.js');

// Standard usage with defaults
vi.mock('@directus/env', async () => {
	const { mockEnv } = await import('../test-utils/env.js');
	return mockEnv();
});

// With custom default values
vi.mock('@directus/env', async () => {
	const { mockEnv } = await import('../test-utils/env.js');
	return mockEnv({
		STORAGE_LOCATIONS: 'custom-storage',
		FILES_DELETE_ORIGINAL_ON_MOVE: 'true',
	});
});

beforeEach(() => {
	resetEnvMock();
});

it('should use custom env value', async () => {
	// Override the mock return value
	vi.mocked(useEnv).mockReturnValue({
		FILES_DELETE_ORIGINAL_ON_MOVE: 'true',
	});

	// Re-import the module to pick up the new mock
	// Required if useEnv is called top level
	const { FilesService } = await import('./files.js');

	// Create new service instance
	const service = new FilesService({
		knex: db,
		schema: { collections: {}, relations: [] },
	});

	// ... rest of test
});

Important Notes:

  • vi.resetModules() in beforeEach is essential for per-test overrides to work
  • Must re-import modules after changing mock values using dynamic import()

storage.ts

Provides storage driver and storage manager mocking utilities for the @directus/storage package.

createMockDriver()

Creates a mock Driver with common storage operations. All methods return sensible defaults and can be customized per-test using vi.mocked().

Returns: Mock Driver instance with the following methods:

  • read → PassThrough stream
  • write → resolves to undefined
  • stat → resolves to { size: 0, modified: new Date() }
  • exists → resolves to false
  • move → resolves to undefined
  • delete → resolves to undefined
  • copy → resolves to undefined
  • list → async generator (yields no files by default)

Example:

import { createMockDriver } from '../test-utils/storage.js';

// Create driver with default behavior
const mockDriver = createMockDriver();

// Customize specific methods for your test using vi.mocked()
vi.mocked(mockDriver.stat).mockResolvedValue({ size: 1024, modified: new Date() });
vi.mocked(mockDriver.exists).mockResolvedValue(true);
vi.mocked(mockDriver.list).mockImplementation(async function* () {
	yield 'file1.jpg';
	yield 'file2.jpg';
});

createMockStorage(driver?)

Creates a mock StorageManager with a location method. By default, creates and returns a standard mock driver for any location.

Parameters:

  • driver (optional): Driver to return from location(). If not provided, creates a default mock driver

Returns: Mock StorageManager instance with the following methods:

  • location(name: string) → returns the provided driver or a default mock driver
  • registerDriver → vi.fn()
  • registerLocation → vi.fn()

Example:

import { createMockDriver, createMockStorage } from '../test-utils/storage.js';

// Basic usage - auto-creates a default driver
const mockStorage = createMockStorage();

// With explicit driver instance for per-test customization
const mockDriver = createMockDriver();
const mockStorage = createMockStorage(mockDriver);

// Now you can modify mockDriver behavior in individual tests using vi.mocked()
vi.mocked(mockDriver.exists).mockResolvedValue(true);

mockStorage(driver?)

Creates a complete storage module mock for use with vi.mock('../storage/index.js'). Returns both getStorage and _cache exports.

Parameters:

  • driver (optional): Custom driver to use

Returns: Mock module object for vi.mock()

Example:

// Basic setup - all tests use same driver behavior
import { getStorage } from '../storage/index.js';

vi.mock('../storage/index.js');

beforeEach(() => {
	const mockStorage = createMockStorage();
	vi.mocked(getStorage).mockResolvedValue(mockStorage);
});

Per-Test Driver Customization Pattern

The recommended pattern for modifying driver behavior in individual tests:

Example:

import { getStorage } from '../storage/index.js';
import type { Driver, StorageManager } from '@directus/storage';
import { createMockDriver, createMockStorage } from '../test-utils/storage.js';

vi.mock('../storage/index.js');

describe('updateMany', () => {
	let service: FilesService;
	let mockDriver: Driver;
	let mockStorage: StorageManager;

	beforeEach(() => {
		service = new FilesService({
			knex: db,
			schema: { collections: {}, relations: [] },
		});

		// Create default mock
		mockDriver = createMockDriver();
		mockStorage = createMockStorage(mockDriver);
		vi.mocked(getStorage).mockResolvedValue(mockStorage);
	});

	it('should move file when filename changes', async () => {
		// Customize driver behavior for this specific test using vi.mocked()
		vi.mocked(mockDriver.list).mockImplementation(async function* () {
			yield 'old-file.jpg';
		});

		await service.updateMany([1], { filename_disk: 'new-file.jpg' });

		expect(mockDriver.move).toHaveBeenCalledWith('old-file.jpg', 'new-file.jpg');
	});

	it('should delete original when remote exists', async () => {
		// Different behavior for this test using vi.mocked()
		vi.mocked(mockDriver.exists).mockResolvedValue(true);
		vi.mocked(mockDriver.list).mockImplementation(async function* () {
			yield 'old-file.jpg';
		});

		await service.updateMany([1], { filename_disk: 'new-file.jpg' });

		expect(mockDriver.delete).toHaveBeenCalledWith('old-file.jpg');
	});
});

items-service.ts

Provides ItemsService mocking utilities for testing services that depend on ItemsService.

mockItemsService()

Creates a standard ItemsService mock with all CRUD methods pre-configured with sensible defaults.

Returns: Mock module object with ItemsService class

Default return values:

  • createOne1
  • createMany[1]
  • readByQuery[]
  • readOne{}
  • readMany[]
  • updateOne1
  • updateMany[1]
  • updateByQuery[1]
  • deleteOne1
  • deleteMany[1]
  • deleteByQuery[1]

Example:

// Standard usage
vi.mock('./items.js', async () => {
	const { mockItemsService } = await import('../test-utils/services/items-service.js');
	return mockItemsService();
});

// Override specific methods during tests
import { ItemsService } from './items.js';

const createOneSpy = vi.spyOn(ItemsService.prototype, 'createOne').mockResolvedValue('custom-id');

await service.createOne(data);
expect(createOneSpy).toHaveBeenCalledWith(expect.objectContaining({ field: 'value' }));

// Mock readByQuery to return specific data
vi.spyOn(ItemsService.prototype, 'readByQuery').mockResolvedValue([
	{ collection: 'users', name: 'John' },
	{ collection: 'posts', name: 'My Post' },
]);

fields-service.ts

Provides FieldsService mocking utilities for testing services that depend on FieldsService (like CollectionsService).

mockFieldsService()

Creates a standard FieldsService mock with common methods pre-configured.

Returns: Mock module object with FieldsService class

Mocked methods:

In addition to the base ItemsService method the following FieldsService specific methods are available:

  • addColumnToTable → no-op function
  • addColumnIndex → resolves to undefined
  • deleteField → resolves to undefined
  • createField → resolves to undefined
  • updateField → resolves to 'field'

Example:

// Standard usage in CollectionsService tests
vi.mock('./fields.js', async () => {
  const { mockFieldsService } = await import('../test-utils/services/fields-service.js');
  return mockFieldsService();
});

// Override specific methods during tests
import { FieldsService } from './fields.js';

const addColumnIndexSpy = vi.spyOn(FieldsService.prototype, 'addColumnIndex')
  .mockResolvedValue();

await service.createOne({ collection: 'test', fields: [...] });
expect(addColumnIndexSpy).toHaveBeenCalled();

files-service.ts

Provides FilesService mocking utilities for testing services that depend on FilesService.

mockFilesService()

Creates a standard FilesService mock with common methods pre-configured.

Returns: Mock module object with FilesService class

Mocked methods:

In addition to the base ItemsService method the following FilesService specific methods are available:

  • uploadOne1
  • importOne1

Example:

// Standard usage in service tests
vi.mock('./files.js', async () => {
	const { mockFilesService } = await import('../test-utils/services/files-service.js');
	return mockFilesService();
});

// Override specific methods during tests
import { FilesService } from './files.js';

const uploadOneSpy = vi.spyOn(FilesService.prototype, 'uploadOne').mockResolvedValue(`1`);

folders-service.ts

Provides FoldersService mocking utilities for testing services that depend on FoldersService.

mockFilesService()

Creates a standard FoldersService mock with common methods pre-configured.

Returns: Mock module object with FoldersService class

Mocked methods:

In addition to the base ItemsService method the following FoldersService specific methods are available:

  • buildTree → return 1 => root map

Example:

// Standard usage in service tests
vi.mock('./folders.js', async () => {
	const { mockFoldersService } = await import('../test-utils/services/folders-service.js');
	return mockFilesService();
});

// Override specific methods during tests
import { FoldersService } from './folders.js';

const buildTreeSpy = vi.spyOn(FoldersService.prototype, 'buildTree').mockResolvedValue(new Map('1', 'root-alt'));

controllers.ts

Provides helpers for extracting route handlers from Express routers and creating mock Express request/response objects for controller tests.

getRouteHandler(router, method, path)

Extracts the middleware/handler stack for a specific route from an Express router.

Parameters:

  • router: The Express Router instance
  • method: HTTP method ('GET', 'POST', 'PATCH', 'DELETE', etc.)
  • path: Route path (e.g. '/', '/:id')

Returns: Array of { handle: (...args) => any } layers for the matched route

Throws: If no matching route is found

Example:

import { default as router } from './tus.js';
import { getRouteHandler } from '../test-utils/controllers.js';

const [checkAccess, handler] = getRouteHandler(router, 'POST', '/');
await checkAccess?.handle(req, res, next);

createMockRequest(overrides?)

Creates a mock Express Request pre-populated with common Directus properties (accountability, schema, sanitizedQuery, etc.).

Parameters:

  • overrides (optional): Properties to merge into the mock request

Returns: Mock Request object

Example:

import { createMockRequest } from '../test-utils/controllers.js';

// Minimal request
const req = createMockRequest({ schema });

// With accountability and custom header
const req = createMockRequest({
	method: 'POST',
	accountability,
	schema,
	header: vi.fn().mockReturnValue('some-value'),
});

createMockResponse(overrides?)

Creates a mock Express Response with chainable methods (status, json, send, set, end).

Parameters:

  • overrides (optional): Properties to merge into the mock response

Returns: Mock Response object

Example:

import { createMockResponse } from '../test-utils/controllers.js';

const res = createMockResponse();

Full Controller Test Example

import { SchemaBuilder } from '@directus/schema-builder';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { createMockRequest, createMockResponse, getRouteHandler } from '../test-utils/controllers.js';
import { default as router } from './controller.js';

const schema = new SchemaBuilder()
	.collection('collection', (c) => {
		c.field('id').integer().primary();
		c.field('title').string();
	})
	.build();

describe('controller', () => {
	beforeEach(() => {
		vi.clearAllMocks();
	});

	test('validates access on POST', async () => {
		const req = createMockRequest({ method: 'POST', accountability, schema });
		const res = createMockResponse();
		const next = vi.fn();

		const [firstHandler] = getRouteHandler(router, 'POST', '/');
		await firstHandler?.handle(req, res, next);

		expect(next).toHaveBeenCalled();
	});
});

Common Patterns

Full Service Test Setup

import { createMockKnex, resetKnexMocks } from '../test-utils/knex.js';
import { SchemaBuilder } from '@directus/schema-builder';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { YourService } from './your-service.js';
import { getStorage } from '../storage/index.js';
import { createMockStorage } from '../test-utils/storage.js';

// Mock all dependencies
vi.mock('../../src/database/index', async () => {
	const { mockDatabase } = await import('../test-utils/database.js');
	return mockDatabase();
});

vi.mock('@directus/schema', async () => {
	const { mockSchema } = await import('../test-utils/schema.js');
	return mockSchema();
});

vi.mock('../cache.js', async () => {
	const { mockCache } = await import('../test-utils/cache.js');
	return mockCache();
});

vi.mock('../emitter.js', async () => {
	const { mockEmitter } = await import('../test-utils/emitter.js');
	return mockEmitter();
});

vi.mock('./items.js', async () => {
	const { mockItemsService } = await import('../test-utils/services/items-service.js');
	return mockItemsService();
});

vi.mock('../storage/index.js');

vi.mock('../utils/transaction.js', async () => {
	const { mockTransaction } = await import('../test-utils/database.js');
	return mockTransaction();
});

// Define test schema
const schema = new SchemaBuilder()
	.collection('users', (c) => {
		c.field('id').integer().primary();
		c.field('name').string();
	})
	.build();

describe('Integration Tests', () => {
	const { db, tracker, mockSchemaBuilder } = createMockKnex();

	beforeEach(() => {
		// Set up storage mock if service uses storage
		const mockStorage = createMockStorage();
		vi.mocked(getStorage).mockResolvedValue(mockStorage);
	});

	afterEach(() => {
		resetKnexMocks(tracker, mockSchemaBuilder);
	});

	describe('Services / YourService', () => {
		test('should create a user', async () => {
			tracker.on.select('users').response([]);
			tracker.on.insert('users').response([1]);

			const service = new YourService({ knex: db, schema });
			const result = await service.createOne({ name: 'John' });

			expect(result).toBe(1);
		});
	});
});

Testing Schema Operations

import { mockCreateTable, mockAlterTable, createMockTableBuilder } from '../test-utils/knex.js';

test('should create table with correct schema', async () => {
	const { db, mockSchemaBuilder } = createMockKnex();
	const service = new YourService({ knex: db, schema });

	await service.createCollection('users');

	expect(mockSchemaBuilder.createTable).toHaveBeenCalledWith('users', expect.any(Function));
});

test('should alter table to add column', async () => {
	const { db } = createMockKnex();
	const alterTableSpy = mockAlterTable();
	db.schema.alterTable = alterTableSpy as any;

	const service = new YourService({ knex: db, schema });
	await service.addField('users', { field: 'email', type: 'string' });

	expect(alterTableSpy).toHaveBeenCalledWith('users', expect.any(Function));
});

Testing Cache Clearing

import { getCache } from '../cache.js';
import { mockCache } from '../test-utils/cache.js';

test('should clear cache after update', async () => {
	const { spies } = mockCache();
	vi.mocked(getCache).mockReturnValue(spies.mockCacheReturn as any);

	const service = new YourService({ knex: db, schema });
	await service.updateOne('1', { name: 'Updated' });

	expect(spies.clearSpy).toHaveBeenCalled();
});

Testing with Storage Operations

import { getStorage } from '../storage/index.js';
import type { Driver, StorageManager } from '@directus/storage';
import { createMockDriver, createMockStorage } from '../test-utils/storage.js';

vi.mock('../storage/index.js');

describe('FilesService', () => {
	let mockDriver: Driver;
	let mockStorage: StorageManager;

	beforeEach(() => {
		mockDriver = createMockDriver();
		mockStorage = createMockStorage(mockDriver);
		vi.mocked(getStorage).mockResolvedValue(mockStorage);
	});

	test('should read file from storage', async () => {
		const mockStream = new PassThrough();
		vi.mocked(mockDriver.read).mockResolvedValue(mockStream);

		const service = new FilesService({ knex: db, schema });
		const stream = await service.getFileStream('test.jpg');

		expect(mockDriver.read).toHaveBeenCalledWith('test.jpg');
		expect(stream).toBe(mockStream);
	});

	test('should move file when renaming', async () => {
		// Customize list to return the file we want to move
		vi.mocked(mockDriver.list).mockImplementation(async function* () {
			yield 'old-name.jpg';
		});

		const service = new FilesService({ knex: db, schema });
		await service.renameFile('old-name.jpg', 'new-name.jpg');

		expect(mockDriver.move).toHaveBeenCalledWith('old-name.jpg', 'new-name.jpg');
	});

	test('should check file existence before writing', async () => {
		vi.mocked(mockDriver.exists).mockResolvedValue(false);

		const service = new FilesService({ knex: db, schema });
		const canWrite = await service.canWriteFile('new-file.jpg');

		expect(mockDriver.exists).toHaveBeenCalledWith('new-file.jpg');
		expect(canWrite).toBe(true);
	});

	test('should get file stats', async () => {
		const mockStats = { size: 2048, modified: new Date('2024-01-01') };
		vi.mocked(mockDriver.stat).mockResolvedValue(mockStats);

		const service = new FilesService({ knex: db, schema });
		const stats = await service.getFileStats('test.jpg');

		expect(mockDriver.stat).toHaveBeenCalledWith('test.jpg');
		expect(stats.size).toBe(2048);
	});
});

Testing with Accountability

test('should allow admin to create collection', async () => {
	const service = new CollectionsService({
		knex: db,
		schema,
		accountability: { role: 'admin', admin: true } as Accountability,
	});

	await expect(service.createOne({ collection: 'test' })).resolves.toBeDefined();
});

test('should deny non-admin from creating collection', async () => {
	const service = new CollectionsService({
		knex: db,
		schema,
		accountability: { role: 'editor', admin: false } as Accountability,
	});

	await expect(service.createOne({ collection: 'test' })).rejects.toThrow(ForbiddenError);
});

test('should read column info', async () => {
	const mockColumns = [
		createMockColumn({ table: 'users', name: 'id', data_type: 'integer', is_primary_key: true }),
		createMockColumn({ table: 'users', name: 'email', data_type: 'varchar', max_length: 255 }),
	];

	service.schemaInspector.columnInfo = vi.fn().mockResolvedValue(mockColumns);
	const result = await service.columnInfo('users');

	expect(result).toEqual(mockColumns);
});

Testing with System Collection Mocks

import { setupSystemCollectionMocks } from '../test-utils/knex.js';

describe('Service Tests', () => {
	const { db, tracker, mockSchemaBuilder } = createMockKnex();

	beforeEach(() => {
		// Automatically mock all CRUD operations for system collections
		setupSystemCollectionMocks(tracker);
	});

	test('should query directus_fields', async () => {
		// Override the default empty response for specific tests
		tracker.on.select('directus_fields').response([{ id: 1, collection: 'users', field: 'name' }]);

		const service = new YourService({ knex: db, schema });
		const fields = await service.getFields('users');

		expect(fields).toHaveLength(1);
	});
});

Mocking Additional Dependencies

// Mock environment variables (using utility)
vi.mock('@directus/env', async () => {
	const { mockEnv } = await import('../test-utils/env.js');
	return mockEnv({
		CACHE_SCHEMA: true,
		DB_CLIENT: 'postgres',
		STORAGE_LOCATIONS: 'local',
	});
});

// Or inline (for quick tests)
vi.mock('@directus/env', () => ({
	useEnv: vi.fn().mockReturnValue({
		CACHE_SCHEMA: true,
		DB_CLIENT: 'postgres',
	}),
}));

// Mock getSchema utility
vi.mock('../utils/get-schema.js', () => ({
	getSchema: vi.fn(),
}));

// Mock permissions utilities
vi.mock('../permissions/lib/fetch-permissions.js', () => ({
	fetchPermissions: vi.fn().mockResolvedValue([
		{
			collection: 'test_collection',
			fields: ['*'],
			action: 'read',
		},
	]),
}));

// Use in tests
import { getSchema } from '../utils/get-schema.js';
vi.mocked(getSchema).mockResolvedValue(schema);

Examples

See these files for complete examples:

Best Practices

Mock Declaration Order

Always declare vi.mock() calls before importing the modules they mock:

// ✅ Correct - mocks first
vi.mock('../cache.js', async () => {
	const { mockCache } = await import('../test-utils/cache.js');
	return mockCache();
});

import { YourService } from './your-service.js';

// ❌ Wrong - imports before mocks
import { YourService } from './your-service.js';

vi.mock('../cache.js', async () => {
	const { mockCache } = await import('../test-utils/cache.js');
	return mockCache();
});

Resetting Mocks Between Tests

Always reset mocks in afterEach to prevent test interference:

afterEach(() => {
	resetKnexMocks(tracker, mockSchemaBuilder);
	vi.clearAllMocks(); // Clear any additional spies
});

Tracker Response Patterns

Set up tracker responses for every database query your test will execute:

test('should create and read item', async () => {
	// Mock all queries that will run
	tracker.on.select('users').response([]); // Check if exists
	tracker.on.insert('users').response([1]); // Create
	tracker.on.select('users').response([{ id: 1, name: 'John' }]); // Read back

	// Your test code...
});

Using TypeScript with Mocks

Add type assertions when needed to satisfy TypeScript:

vi.mocked(getCache).mockReturnValue({
	cache: { clear: vi.fn() },
	systemCache: { clear: vi.fn() },
	localSchemaCache: { get: vi.fn(), set: vi.fn() },
	lockCache: undefined,
} as any);

Spying on Service Methods

Use vi.spyOn to override specific methods while keeping the rest of the service intact:

import { ItemsService } from './items.js';

const createOneSpy = vi.spyOn(ItemsService.prototype, 'createOne').mockResolvedValue('new-id');

// Test your code
expect(createOneSpy).toHaveBeenCalledWith(expect.objectContaining({ field: 'value' }));

Contributing

When adding new mock utilities:

  • Create or update the appropriate mock file
  • Add comprehensive JSDoc comments with examples
  • Document the utility in this README
  • Export all functions from the module
  • Use TypeScript for proper typing
  • Include return type documentation
  • Test the mock with actual service tests

Keywords

directus

FAQs

Package last updated on 30 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts