@directus/api 31.0.0 → 32.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +2 -0
- package/dist/auth/auth.d.ts +2 -1
- package/dist/auth/auth.js +7 -2
- package/dist/auth/drivers/ldap.d.ts +0 -2
- package/dist/auth/drivers/ldap.js +9 -7
- package/dist/auth/drivers/oauth2.d.ts +0 -2
- package/dist/auth/drivers/oauth2.js +11 -8
- package/dist/auth/drivers/openid.d.ts +0 -2
- package/dist/auth/drivers/openid.js +11 -8
- package/dist/auth/drivers/saml.d.ts +0 -2
- package/dist/auth/drivers/saml.js +5 -5
- package/dist/auth.js +1 -2
- package/dist/cli/commands/bootstrap/index.js +12 -33
- package/dist/cli/commands/init/index.js +1 -1
- package/dist/cli/commands/schema/apply.d.ts +4 -0
- package/dist/cli/commands/schema/apply.js +26 -3
- package/dist/controllers/collections.js +7 -2
- package/dist/controllers/fields.js +31 -8
- package/dist/controllers/server.js +26 -1
- package/dist/controllers/settings.js +9 -2
- package/dist/controllers/users.js +2 -2
- package/dist/database/helpers/fn/types.js +3 -3
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +23 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +25 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/oracle.js +13 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/postgres.js +13 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +6 -0
- package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
- package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
- package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
- package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
- package/dist/database/run-ast/lib/apply-query/index.js +4 -6
- package/dist/database/run-ast/lib/apply-query/search.js +2 -0
- package/dist/database/run-ast/lib/get-db-query.js +7 -6
- package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
- package/dist/database/run-ast/utils/generate-alias.js +57 -0
- package/dist/flows.js +1 -0
- package/dist/mcp/schema.d.ts +14 -14
- package/dist/mcp/schema.js +6 -6
- package/dist/mcp/server.d.ts +9 -3
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools/collections.d.ts +1 -1
- package/dist/mcp/tools/fields.d.ts +1 -1
- package/dist/mcp/tools/files.d.ts +25 -25
- package/dist/mcp/tools/flows.d.ts +36 -36
- package/dist/mcp/tools/folders.d.ts +18 -18
- package/dist/mcp/tools/items.d.ts +18 -18
- package/dist/mcp/tools/operations.d.ts +19 -19
- package/dist/mcp/tools/prompts/items.md +1 -1
- package/dist/metrics/lib/create-metrics.js +16 -25
- package/dist/middleware/collection-exists.js +2 -2
- package/dist/operations/mail/index.js +3 -1
- package/dist/operations/mail/rate-limiter.d.ts +1 -0
- package/dist/operations/mail/rate-limiter.js +29 -0
- package/dist/permissions/modules/process-payload/process-payload.js +3 -10
- package/dist/permissions/modules/validate-access/validate-access.js +2 -3
- package/dist/schedules/metrics.js +6 -2
- package/dist/schedules/project.d.ts +4 -0
- package/dist/schedules/project.js +27 -0
- package/dist/services/collections.d.ts +3 -3
- package/dist/services/collections.js +16 -1
- package/dist/services/fields.d.ts +21 -5
- package/dist/services/fields.js +105 -28
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/graphql/resolvers/system-admin.js +49 -5
- package/dist/services/graphql/schema/parse-query.js +8 -8
- package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
- package/dist/services/graphql/utils/aggregate-query.js +5 -1
- package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
- package/dist/services/import-export.d.ts +9 -1
- package/dist/services/import-export.js +318 -101
- package/dist/services/items.d.ts +1 -1
- package/dist/services/items.js +36 -20
- package/dist/services/mail/index.js +2 -0
- package/dist/services/mail/rate-limiter.d.ts +1 -0
- package/dist/services/mail/rate-limiter.js +29 -0
- package/dist/services/meta.js +28 -24
- package/dist/services/schema.js +4 -1
- package/dist/services/server.d.ts +1 -0
- package/dist/services/server.js +14 -18
- package/dist/services/settings.d.ts +2 -1
- package/dist/services/settings.js +15 -0
- package/dist/services/tus/server.js +14 -9
- package/dist/telemetry/lib/get-report.js +4 -4
- package/dist/telemetry/lib/send-report.d.ts +6 -1
- package/dist/telemetry/lib/send-report.js +3 -1
- package/dist/telemetry/types/report.d.ts +17 -1
- package/dist/telemetry/utils/get-settings.d.ts +9 -0
- package/dist/telemetry/utils/get-settings.js +14 -0
- package/dist/test-utils/README.md +760 -0
- package/dist/test-utils/cache.d.ts +51 -0
- package/dist/test-utils/cache.js +59 -0
- package/dist/test-utils/database.d.ts +48 -0
- package/dist/test-utils/database.js +52 -0
- package/dist/test-utils/emitter.d.ts +35 -0
- package/dist/test-utils/emitter.js +38 -0
- package/dist/test-utils/fields-service.d.ts +28 -0
- package/dist/test-utils/fields-service.js +36 -0
- package/dist/test-utils/items-service.d.ts +23 -0
- package/dist/test-utils/items-service.js +37 -0
- package/dist/test-utils/knex.d.ts +164 -0
- package/dist/test-utils/knex.js +268 -0
- package/dist/test-utils/schema.d.ts +26 -0
- package/dist/test-utils/schema.js +35 -0
- package/dist/types/auth.d.ts +0 -2
- package/dist/utils/apply-diff.js +15 -0
- package/dist/utils/create-admin.d.ts +11 -0
- package/dist/utils/create-admin.js +50 -0
- package/dist/utils/get-schema.js +5 -3
- package/dist/utils/get-snapshot-diff.js +49 -5
- package/dist/utils/get-snapshot.js +13 -7
- package/dist/utils/sanitize-schema.d.ts +11 -4
- package/dist/utils/sanitize-schema.js +9 -6
- package/dist/utils/schedule.js +15 -19
- package/dist/utils/validate-diff.js +31 -0
- package/dist/utils/validate-snapshot.js +7 -0
- package/dist/websocket/controllers/hooks.js +12 -20
- package/dist/websocket/messages.d.ts +3 -3
- package/package.json +63 -65
- package/dist/cli/utils/defaults.d.ts +0 -4
- package/dist/cli/utils/defaults.js +0 -17
- package/dist/telemetry/utils/get-project-id.d.ts +0 -2
- package/dist/telemetry/utils/get-project-id.js +0 -4
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
# Service Test Mocks
|
|
2
|
+
|
|
3
|
+
Shared mocking utilities for service layer tests. These utilities help reduce code duplication and provide consistent
|
|
4
|
+
mocking patterns across service tests.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
This directory contains mock implementations for commonly used modules in service tests:
|
|
9
|
+
|
|
10
|
+
- **[knex.ts](#knexts)** - Knex instance, query tracker, and schema builder mocks
|
|
11
|
+
- **[database.ts](#databasets)** - Database client and transaction mocks
|
|
12
|
+
- **[cache.ts](#cachets)** - Cache system mocks
|
|
13
|
+
- **[schema.ts](#schemats)** - Schema inspector mocks
|
|
14
|
+
- **[emitter.ts](#emitterts)** - Event emitter mocks
|
|
15
|
+
- **[items-service.ts](#items-servicets)** - ItemsService mocks
|
|
16
|
+
- **[fields-service.ts](#fields-servicets)** - FieldsService mocks
|
|
17
|
+
- **[test-helpers.ts](#test-helpersts)** - Test data factory functions
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { createMockKnex, resetKnexMocks } from '../__mocks__/knex.js';
|
|
23
|
+
|
|
24
|
+
// Set up mocks
|
|
25
|
+
vi.mock('../../src/database/index', async () => {
|
|
26
|
+
const { mockDatabase } = await import('../__mocks__/database.js');
|
|
27
|
+
return mockDatabase();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
vi.mock('../cache.js', async () => {
|
|
31
|
+
const { mockCache } = await import('../__mocks__/cache.js');
|
|
32
|
+
return mockCache();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('YourService Tests', () => {
|
|
36
|
+
const { db, tracker, mockSchemaBuilder } = createMockKnex();
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
resetKnexMocks(tracker, mockSchemaBuilder);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Your tests here
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## File Documentation
|
|
47
|
+
|
|
48
|
+
### knex.ts
|
|
49
|
+
|
|
50
|
+
Provides Knex mocking utilities including the database client, query tracker, and schema builder.
|
|
51
|
+
|
|
52
|
+
#### `createMockKnex()`
|
|
53
|
+
|
|
54
|
+
Creates a complete mocked Knex instance with tracker and schema builder support.
|
|
55
|
+
|
|
56
|
+
**Returns:** `{ db, tracker, mockSchemaBuilder }`
|
|
57
|
+
|
|
58
|
+
- `db`: Mocked Knex client (connected to knex-mock-client)
|
|
59
|
+
- `tracker`: Query tracker for mocking database responses
|
|
60
|
+
- `mockSchemaBuilder`: Mocked schema builder with createTable, dropTable, alterTable, etc.
|
|
61
|
+
|
|
62
|
+
**Example:**
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const { db, tracker, mockSchemaBuilder } = createMockKnex();
|
|
66
|
+
|
|
67
|
+
// Mock query responses
|
|
68
|
+
tracker.on.select('users').response([{ id: 1, name: 'John' }]);
|
|
69
|
+
tracker.on.insert('users').response([1]);
|
|
70
|
+
|
|
71
|
+
// Use in service constructor
|
|
72
|
+
const service = new YourService({ knex: db, schema });
|
|
73
|
+
|
|
74
|
+
// Verify schema operations
|
|
75
|
+
expect(mockSchemaBuilder.createTable).toHaveBeenCalledWith('users', expect.any(Function));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### `createMockTableBuilder()`
|
|
79
|
+
|
|
80
|
+
Creates a mock Knex table builder with all column types, modifiers, and constraints.
|
|
81
|
+
|
|
82
|
+
**Returns:** Mock table builder with chainable methods
|
|
83
|
+
|
|
84
|
+
**Example:**
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
const table = createMockTableBuilder();
|
|
88
|
+
table.string('name', 255).notNullable().index();
|
|
89
|
+
table.integer('age').unsigned().nullable();
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### `setupSystemCollectionMocks(tracker)`
|
|
93
|
+
|
|
94
|
+
Sets up default CRUD operation mocks for all Directus system collections (directus_collections, directus_fields, etc.).
|
|
95
|
+
|
|
96
|
+
**Parameters:**
|
|
97
|
+
|
|
98
|
+
- `tracker`: The knex-mock-client tracker instance
|
|
99
|
+
|
|
100
|
+
**Example:**
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const { tracker } = createMockKnex();
|
|
104
|
+
setupSystemCollectionMocks(tracker);
|
|
105
|
+
// Now all system collection queries return empty arrays by default
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### `resetKnexMocks(tracker, mockSchemaBuilder)`
|
|
109
|
+
|
|
110
|
+
Resets all mock states. Should be called in `afterEach` hooks.
|
|
111
|
+
|
|
112
|
+
**Parameters:**
|
|
113
|
+
|
|
114
|
+
- `tracker`: The knex-mock-client tracker instance
|
|
115
|
+
- `mockSchemaBuilder`: The mock schema builder object from createMockKnex
|
|
116
|
+
|
|
117
|
+
**Example:**
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const { db, tracker, mockSchemaBuilder } = createMockKnex();
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
resetKnexMocks(tracker, mockSchemaBuilder);
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### `mockCreateTable()`
|
|
128
|
+
|
|
129
|
+
Creates a mock for `db.schema.createTable` that executes the callback with a mock table builder.
|
|
130
|
+
|
|
131
|
+
**Returns:** `vi.fn()` that can be assigned to `db.schema.createTable`
|
|
132
|
+
|
|
133
|
+
**Example:**
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const { db } = createMockKnex();
|
|
137
|
+
const createTableSpy = mockCreateTable();
|
|
138
|
+
db.schema.createTable = createTableSpy as any;
|
|
139
|
+
|
|
140
|
+
await db.schema.createTable('users', (table) => {
|
|
141
|
+
table.increments('id');
|
|
142
|
+
table.string('name');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(createTableSpy).toHaveBeenCalledWith('users', expect.any(Function));
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### `mockAlterTable()`
|
|
149
|
+
|
|
150
|
+
Creates a mock for `db.schema.alterTable` that executes the callback with a mock table builder.
|
|
151
|
+
|
|
152
|
+
**Returns:** `vi.fn()` that can be assigned to `db.schema.alterTable`
|
|
153
|
+
|
|
154
|
+
**Example:**
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const { db } = createMockKnex();
|
|
158
|
+
const alterTableSpy = mockAlterTable();
|
|
159
|
+
db.schema.alterTable = alterTableSpy as any;
|
|
160
|
+
|
|
161
|
+
await db.schema.alterTable('users', (table) => {
|
|
162
|
+
table.string('email');
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `mockSchemaTable()`
|
|
167
|
+
|
|
168
|
+
Creates a mock for `db.schema.table` that executes the callback with a mock table builder.
|
|
169
|
+
|
|
170
|
+
**Returns:** `vi.fn()` that can be assigned to `db.schema.table`
|
|
171
|
+
|
|
172
|
+
**Example:**
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const { db } = createMockKnex();
|
|
176
|
+
const schemaTableSpy = mockSchemaTable();
|
|
177
|
+
db.schema.table = schemaTableSpy as any;
|
|
178
|
+
|
|
179
|
+
await db.schema.table('users', (table) => {
|
|
180
|
+
table.dropColumn('old_field');
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### database.ts
|
|
187
|
+
|
|
188
|
+
Provides database module mocking utilities including database client detection and transaction handling.
|
|
189
|
+
|
|
190
|
+
#### `mockDatabase(client?)`
|
|
191
|
+
|
|
192
|
+
Creates a standard database module mock for service tests.
|
|
193
|
+
|
|
194
|
+
**Parameters:**
|
|
195
|
+
|
|
196
|
+
- `client` (optional): Database client to mock (default: `'postgres'`)
|
|
197
|
+
- Supported values: `'postgres'`, `'mysql'`, `'sqlite3'`, `'mssql'`, `'oracledb'`, `'cockroachdb'`
|
|
198
|
+
|
|
199
|
+
**Returns:** Mock module object with `getDatabaseClient` and `getSchemaInspector`
|
|
200
|
+
|
|
201
|
+
**Example:**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Standard PostgreSQL mock
|
|
205
|
+
vi.mock('../../src/database/index', async () => {
|
|
206
|
+
const { mockDatabase } = await import('../__mocks__/database.js');
|
|
207
|
+
return mockDatabase();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// MySQL-specific mock
|
|
211
|
+
vi.mock('../../src/database/index', async () => {
|
|
212
|
+
const { mockDatabase } = await import('../__mocks__/database.js');
|
|
213
|
+
return mockDatabase('mysql');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Dynamically change client during tests
|
|
217
|
+
import { getDatabaseClient } from '../../src/database/index.js';
|
|
218
|
+
vi.mocked(getDatabaseClient).mockReturnValue('sqlite3');
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `mockTransaction()`
|
|
222
|
+
|
|
223
|
+
Creates a mock for the transaction utility. By default, executes the callback with the provided Knex instance (no actual
|
|
224
|
+
transaction wrapper).
|
|
225
|
+
|
|
226
|
+
**Returns:** Mock module object with `transaction` function
|
|
227
|
+
|
|
228
|
+
**Example:**
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
vi.mock('../utils/transaction.js', async () => {
|
|
232
|
+
const { mockTransaction } = await import('../__mocks__/database.js');
|
|
233
|
+
return mockTransaction();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Transaction callback is executed immediately with the same knex instance
|
|
237
|
+
await transaction(db, async (trx) => {
|
|
238
|
+
// trx === db in tests
|
|
239
|
+
await trx('users').insert({ name: 'John' });
|
|
240
|
+
});
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### cache.ts
|
|
246
|
+
|
|
247
|
+
Provides cache system mocking utilities.
|
|
248
|
+
|
|
249
|
+
#### `mockCache()`
|
|
250
|
+
|
|
251
|
+
Creates a standard cache module mock with getCache, getCacheValue, setCacheValue, and clearSystemCache. Returns both the
|
|
252
|
+
mocks for vi.mock() declarations and spies for testing cache behavior.
|
|
253
|
+
|
|
254
|
+
**Returns:** Object with mock functions and spies
|
|
255
|
+
|
|
256
|
+
- `getCache`: Mock function returning cache object
|
|
257
|
+
- `getCacheValue`: Mock function returning null
|
|
258
|
+
- `setCacheValue`: Mock function returning undefined
|
|
259
|
+
- `clearSystemCache`: Mock function
|
|
260
|
+
- `spies`: Spy functions for testing cache behavior
|
|
261
|
+
- `clearSpy`: Spy for cache.clear()
|
|
262
|
+
- `systemClearSpy`: Spy for systemCache.clear()
|
|
263
|
+
- `getCacheSpy`: Spy for localSchemaCache.get()
|
|
264
|
+
- `setCacheSpy`: Spy for localSchemaCache.set()
|
|
265
|
+
- `mockCacheReturn`: The mock cache object to pass to vi.mocked()
|
|
266
|
+
|
|
267
|
+
**Example:**
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// Standard usage for vi.mock()
|
|
271
|
+
vi.mock('../cache.js', async () => {
|
|
272
|
+
const { mockCache } = await import('../__mocks__/cache.js');
|
|
273
|
+
return mockCache();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Testing cache clearing with spies
|
|
277
|
+
import { getCache } from '../cache.js';
|
|
278
|
+
import { mockCache } from '../__mocks__/cache.js';
|
|
279
|
+
|
|
280
|
+
test('should clear cache after update', async () => {
|
|
281
|
+
const { spies } = mockCache();
|
|
282
|
+
vi.mocked(getCache).mockReturnValue(spies.mockCacheReturn as any);
|
|
283
|
+
|
|
284
|
+
const service = new YourService({ knex: db, schema });
|
|
285
|
+
await service.updateOne('1', { name: 'Updated' });
|
|
286
|
+
|
|
287
|
+
expect(spies.clearSpy).toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### schema.ts
|
|
294
|
+
|
|
295
|
+
Provides schema inspector mocking utilities for the `@directus/schema` package.
|
|
296
|
+
|
|
297
|
+
#### `mockSchema()`
|
|
298
|
+
|
|
299
|
+
Creates a standard schema inspector mock with tableInfo, columnInfo, primary, foreignKeys, etc.
|
|
300
|
+
|
|
301
|
+
**Returns:** Mock module object for `vi.mock()`
|
|
302
|
+
|
|
303
|
+
**Example:**
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// Standard usage
|
|
307
|
+
vi.mock('@directus/schema', async () => {
|
|
308
|
+
const { mockSchema } = await import('../__mocks__/schema.js');
|
|
309
|
+
return mockSchema();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Dynamically change inspector behavior during tests
|
|
313
|
+
import { createInspector } from '@directus/schema';
|
|
314
|
+
vi.mocked(createInspector).mockReturnValue({
|
|
315
|
+
tableInfo: vi.fn().mockResolvedValue([{ name: 'users' }, { name: 'posts' }]),
|
|
316
|
+
columnInfo: vi.fn().mockResolvedValue([
|
|
317
|
+
{ table: 'users', name: 'id', data_type: 'integer', is_nullable: false },
|
|
318
|
+
{ table: 'users', name: 'name', data_type: 'varchar', is_nullable: true },
|
|
319
|
+
]),
|
|
320
|
+
primary: vi.fn().mockResolvedValue('id'),
|
|
321
|
+
foreignKeys: vi.fn().mockResolvedValue([]),
|
|
322
|
+
withSchema: vi.fn().mockReturnThis(),
|
|
323
|
+
} as any);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### emitter.ts
|
|
329
|
+
|
|
330
|
+
Provides event emitter mocking utilities.
|
|
331
|
+
|
|
332
|
+
#### `mockEmitter()`
|
|
333
|
+
|
|
334
|
+
Creates a standard emitter mock with emitAction, emitFilter, emitInit, and event listener methods.
|
|
335
|
+
|
|
336
|
+
**Returns:** Mock module object for `vi.mock()`
|
|
337
|
+
|
|
338
|
+
**Example:**
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// Standard usage
|
|
342
|
+
vi.mock('../emitter.js', async () => {
|
|
343
|
+
const { mockEmitter } = await import('../__mocks__/emitter.js');
|
|
344
|
+
return mockEmitter();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Dynamically change emitter behavior during tests
|
|
348
|
+
import emitter from '../emitter.js';
|
|
349
|
+
|
|
350
|
+
// Mock filter to modify payload
|
|
351
|
+
vi.mocked(emitter.emitFilter).mockResolvedValue({ modified: true });
|
|
352
|
+
|
|
353
|
+
// Verify action was emitted
|
|
354
|
+
await service.createOne(data);
|
|
355
|
+
expect(emitter.emitAction).toHaveBeenCalledWith(
|
|
356
|
+
'items.create',
|
|
357
|
+
expect.objectContaining({ collection: 'test_collection' }),
|
|
358
|
+
expect.any(Object),
|
|
359
|
+
);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
### items-service.ts
|
|
365
|
+
|
|
366
|
+
Provides ItemsService mocking utilities for testing services that depend on ItemsService.
|
|
367
|
+
|
|
368
|
+
#### `mockItemsService()`
|
|
369
|
+
|
|
370
|
+
Creates a standard ItemsService mock with all CRUD methods pre-configured with sensible defaults.
|
|
371
|
+
|
|
372
|
+
**Returns:** Mock module object with `ItemsService` class
|
|
373
|
+
|
|
374
|
+
**Default return values:**
|
|
375
|
+
|
|
376
|
+
- `createOne` → `1`
|
|
377
|
+
- `createMany` → `[1]`
|
|
378
|
+
- `readByQuery` → `[]`
|
|
379
|
+
- `readOne` → `{}`
|
|
380
|
+
- `readMany` → `[]`
|
|
381
|
+
- `updateOne` → `1`
|
|
382
|
+
- `updateMany` → `[1]`
|
|
383
|
+
- `updateByQuery` → `[1]`
|
|
384
|
+
- `deleteOne` → `1`
|
|
385
|
+
- `deleteMany` → `[1]`
|
|
386
|
+
- `deleteByQuery` → `[1]`
|
|
387
|
+
|
|
388
|
+
**Example:**
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
// Standard usage
|
|
392
|
+
vi.mock('./items.js', async () => {
|
|
393
|
+
const { mockItemsService } = await import('../__mocks__/items-service.js');
|
|
394
|
+
return mockItemsService();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Override specific methods during tests
|
|
398
|
+
import { ItemsService } from './items.js';
|
|
399
|
+
|
|
400
|
+
const createOneSpy = vi.spyOn(ItemsService.prototype, 'createOne').mockResolvedValue('custom-id');
|
|
401
|
+
|
|
402
|
+
await service.createOne(data);
|
|
403
|
+
expect(createOneSpy).toHaveBeenCalledWith(expect.objectContaining({ field: 'value' }));
|
|
404
|
+
|
|
405
|
+
// Mock readByQuery to return specific data
|
|
406
|
+
vi.spyOn(ItemsService.prototype, 'readByQuery').mockResolvedValue([
|
|
407
|
+
{ collection: 'users', name: 'John' },
|
|
408
|
+
{ collection: 'posts', name: 'My Post' },
|
|
409
|
+
]);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### fields-service.ts
|
|
415
|
+
|
|
416
|
+
Provides FieldsService mocking utilities for testing services that depend on FieldsService (like CollectionsService).
|
|
417
|
+
|
|
418
|
+
#### `mockFieldsService()`
|
|
419
|
+
|
|
420
|
+
Creates a standard FieldsService mock with common methods pre-configured.
|
|
421
|
+
|
|
422
|
+
**Returns:** Mock module object with `FieldsService` class
|
|
423
|
+
|
|
424
|
+
**Mocked methods:**
|
|
425
|
+
|
|
426
|
+
- `addColumnToTable` → no-op function
|
|
427
|
+
- `addColumnIndex` → resolves to undefined
|
|
428
|
+
- `deleteField` → resolves to undefined
|
|
429
|
+
- `createField` → resolves to undefined
|
|
430
|
+
- `updateField` → resolves to 'field'
|
|
431
|
+
|
|
432
|
+
**Example:**
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
// Standard usage in CollectionsService tests
|
|
436
|
+
vi.mock('./fields.js', async () => {
|
|
437
|
+
const { mockFieldsService } = await import('../__mocks__/fields-service.js');
|
|
438
|
+
return mockFieldsService();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Override specific methods during tests
|
|
442
|
+
import { FieldsService } from './fields.js';
|
|
443
|
+
|
|
444
|
+
const addColumnIndexSpy = vi.spyOn(FieldsService.prototype, 'addColumnIndex')
|
|
445
|
+
.mockResolvedValue();
|
|
446
|
+
|
|
447
|
+
await service.createOne({ collection: 'test', fields: [...] });
|
|
448
|
+
expect(addColumnIndexSpy).toHaveBeenCalled();
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Common Patterns
|
|
454
|
+
|
|
455
|
+
### Full Service Test Setup
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
import { createMockKnex, resetKnexMocks } from '../__mocks__/knex.js';
|
|
459
|
+
import { SchemaBuilder } from '@directus/schema-builder';
|
|
460
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
461
|
+
|
|
462
|
+
// Mock all dependencies (before imports)
|
|
463
|
+
vi.mock('../../src/database/index', async () => {
|
|
464
|
+
const { mockDatabase } = await import('../__mocks__/database.js');
|
|
465
|
+
return mockDatabase();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
vi.mock('@directus/schema', async () => {
|
|
469
|
+
const { mockSchema } = await import('../__mocks__/schema.js');
|
|
470
|
+
return mockSchema();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
vi.mock('../cache.js', async () => {
|
|
474
|
+
const { mockCache } = await import('../__mocks__/cache.js');
|
|
475
|
+
return mockCache();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
vi.mock('../emitter.js', async () => {
|
|
479
|
+
const { mockEmitter } = await import('../__mocks__/emitter.js');
|
|
480
|
+
return mockEmitter();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
vi.mock('./items.js', async () => {
|
|
484
|
+
const { mockItemsService } = await import('../__mocks__/items-service.js');
|
|
485
|
+
return mockItemsService();
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
vi.mock('../utils/transaction.js', async () => {
|
|
489
|
+
const { mockTransaction } = await import('../__mocks__/database.js');
|
|
490
|
+
return mockTransaction();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// Import after mocks
|
|
494
|
+
import { YourService } from './your-service.js';
|
|
495
|
+
|
|
496
|
+
// Define test schema
|
|
497
|
+
const schema = new SchemaBuilder()
|
|
498
|
+
.collection('users', (c) => {
|
|
499
|
+
c.field('id').integer().primary();
|
|
500
|
+
c.field('name').string();
|
|
501
|
+
})
|
|
502
|
+
.build();
|
|
503
|
+
|
|
504
|
+
describe('Integration Tests', () => {
|
|
505
|
+
const { db, tracker, mockSchemaBuilder } = createMockKnex();
|
|
506
|
+
|
|
507
|
+
afterEach(() => {
|
|
508
|
+
resetKnexMocks(tracker, mockSchemaBuilder);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe('Services / YourService', () => {
|
|
512
|
+
test('should create a user', async () => {
|
|
513
|
+
tracker.on.select('users').response([]);
|
|
514
|
+
tracker.on.insert('users').response([1]);
|
|
515
|
+
|
|
516
|
+
const service = new YourService({ knex: db, schema });
|
|
517
|
+
const result = await service.createOne({ name: 'John' });
|
|
518
|
+
|
|
519
|
+
expect(result).toBe(1);
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
});
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Testing Schema Operations
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
import { mockCreateTable, mockAlterTable, createMockTableBuilder } from '../__mocks__/knex.js';
|
|
529
|
+
|
|
530
|
+
test('should create table with correct schema', async () => {
|
|
531
|
+
const { db, mockSchemaBuilder } = createMockKnex();
|
|
532
|
+
const service = new YourService({ knex: db, schema });
|
|
533
|
+
|
|
534
|
+
await service.createCollection('users');
|
|
535
|
+
|
|
536
|
+
expect(mockSchemaBuilder.createTable).toHaveBeenCalledWith('users', expect.any(Function));
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test('should alter table to add column', async () => {
|
|
540
|
+
const { db } = createMockKnex();
|
|
541
|
+
const alterTableSpy = mockAlterTable();
|
|
542
|
+
db.schema.alterTable = alterTableSpy as any;
|
|
543
|
+
|
|
544
|
+
const service = new YourService({ knex: db, schema });
|
|
545
|
+
await service.addField('users', { field: 'email', type: 'string' });
|
|
546
|
+
|
|
547
|
+
expect(alterTableSpy).toHaveBeenCalledWith('users', expect.any(Function));
|
|
548
|
+
});
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Testing Cache Clearing
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
import { getCache } from '../cache.js';
|
|
555
|
+
import { mockCache } from '../__mocks__/cache.js';
|
|
556
|
+
|
|
557
|
+
test('should clear cache after update', async () => {
|
|
558
|
+
const { spies } = mockCache();
|
|
559
|
+
vi.mocked(getCache).mockReturnValue(spies.mockCacheReturn as any);
|
|
560
|
+
|
|
561
|
+
const service = new YourService({ knex: db, schema });
|
|
562
|
+
await service.updateOne('1', { name: 'Updated' });
|
|
563
|
+
|
|
564
|
+
expect(spies.clearSpy).toHaveBeenCalled();
|
|
565
|
+
});
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Testing with Accountability
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
test('should allow admin to create collection', async () => {
|
|
572
|
+
const service = new CollectionsService({
|
|
573
|
+
knex: db,
|
|
574
|
+
schema,
|
|
575
|
+
accountability: { role: 'admin', admin: true } as Accountability,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
await expect(service.createOne({ collection: 'test' })).resolves.toBeDefined();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('should deny non-admin from creating collection', async () => {
|
|
582
|
+
const service = new CollectionsService({
|
|
583
|
+
knex: db,
|
|
584
|
+
schema,
|
|
585
|
+
accountability: { role: 'editor', admin: false } as Accountability,
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
await expect(service.createOne({ collection: 'test' })).rejects.toThrow(ForbiddenError);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('should read column info', async () => {
|
|
592
|
+
const mockColumns = [
|
|
593
|
+
createMockColumn({ table: 'users', name: 'id', data_type: 'integer', is_primary_key: true }),
|
|
594
|
+
createMockColumn({ table: 'users', name: 'email', data_type: 'varchar', max_length: 255 }),
|
|
595
|
+
];
|
|
596
|
+
|
|
597
|
+
service.schemaInspector.columnInfo = vi.fn().mockResolvedValue(mockColumns);
|
|
598
|
+
const result = await service.columnInfo('users');
|
|
599
|
+
|
|
600
|
+
expect(result).toEqual(mockColumns);
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Testing with System Collection Mocks
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
import { setupSystemCollectionMocks } from '../__mocks__/knex.js';
|
|
608
|
+
|
|
609
|
+
describe('Service Tests', () => {
|
|
610
|
+
const { db, tracker, mockSchemaBuilder } = createMockKnex();
|
|
611
|
+
|
|
612
|
+
beforeEach(() => {
|
|
613
|
+
// Automatically mock all CRUD operations for system collections
|
|
614
|
+
setupSystemCollectionMocks(tracker);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test('should query directus_fields', async () => {
|
|
618
|
+
// Override the default empty response for specific tests
|
|
619
|
+
tracker.on.select('directus_fields').response([{ id: 1, collection: 'users', field: 'name' }]);
|
|
620
|
+
|
|
621
|
+
const service = new YourService({ knex: db, schema });
|
|
622
|
+
const fields = await service.getFields('users');
|
|
623
|
+
|
|
624
|
+
expect(fields).toHaveLength(1);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Mocking Additional Dependencies
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
// Mock environment variables
|
|
633
|
+
vi.mock('@directus/env', () => ({
|
|
634
|
+
useEnv: vi.fn().mockReturnValue({
|
|
635
|
+
CACHE_SCHEMA: true,
|
|
636
|
+
DB_CLIENT: 'postgres',
|
|
637
|
+
}),
|
|
638
|
+
}));
|
|
639
|
+
|
|
640
|
+
// Mock getSchema utility
|
|
641
|
+
vi.mock('../utils/get-schema.js', () => ({
|
|
642
|
+
getSchema: vi.fn(),
|
|
643
|
+
}));
|
|
644
|
+
|
|
645
|
+
// Mock permissions utilities
|
|
646
|
+
vi.mock('../permissions/lib/fetch-permissions.js', () => ({
|
|
647
|
+
fetchPermissions: vi.fn().mockResolvedValue([
|
|
648
|
+
{
|
|
649
|
+
collection: 'test_collection',
|
|
650
|
+
fields: ['*'],
|
|
651
|
+
action: 'read',
|
|
652
|
+
},
|
|
653
|
+
]),
|
|
654
|
+
}));
|
|
655
|
+
|
|
656
|
+
// Use in tests
|
|
657
|
+
import { getSchema } from '../utils/get-schema.js';
|
|
658
|
+
vi.mocked(getSchema).mockResolvedValue(schema);
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## Examples
|
|
664
|
+
|
|
665
|
+
See these files for complete examples:
|
|
666
|
+
|
|
667
|
+
- [collections.test.ts](../services/collections.test.ts) - Full service test with schema operations
|
|
668
|
+
- [fields.test.ts](../services/fields.test.ts) - Complex service test with field management
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## Best Practices
|
|
673
|
+
|
|
674
|
+
### Mock Declaration Order
|
|
675
|
+
|
|
676
|
+
Always declare `vi.mock()` calls **before** importing the modules they mock:
|
|
677
|
+
|
|
678
|
+
```typescript
|
|
679
|
+
// ✅ Correct - mocks first
|
|
680
|
+
vi.mock('../cache.js', async () => {
|
|
681
|
+
const { mockCache } = await import('../__mocks__/cache.js');
|
|
682
|
+
return mockCache();
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
import { YourService } from './your-service.js';
|
|
686
|
+
|
|
687
|
+
// ❌ Wrong - imports before mocks
|
|
688
|
+
import { YourService } from './your-service.js';
|
|
689
|
+
|
|
690
|
+
vi.mock('../cache.js', async () => {
|
|
691
|
+
const { mockCache } = await import('../__mocks__/cache.js');
|
|
692
|
+
return mockCache();
|
|
693
|
+
});
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Resetting Mocks Between Tests
|
|
697
|
+
|
|
698
|
+
Always reset mocks in `afterEach` to prevent test interference:
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
afterEach(() => {
|
|
702
|
+
resetKnexMocks(tracker, mockSchemaBuilder);
|
|
703
|
+
vi.clearAllMocks(); // Clear any additional spies
|
|
704
|
+
});
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Tracker Response Patterns
|
|
708
|
+
|
|
709
|
+
Set up tracker responses for every database query your test will execute:
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
test('should create and read item', async () => {
|
|
713
|
+
// Mock all queries that will run
|
|
714
|
+
tracker.on.select('users').response([]); // Check if exists
|
|
715
|
+
tracker.on.insert('users').response([1]); // Create
|
|
716
|
+
tracker.on.select('users').response([{ id: 1, name: 'John' }]); // Read back
|
|
717
|
+
|
|
718
|
+
// Your test code...
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
### Using TypeScript with Mocks
|
|
723
|
+
|
|
724
|
+
Add type assertions when needed to satisfy TypeScript:
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
vi.mocked(getCache).mockReturnValue({
|
|
728
|
+
cache: { clear: vi.fn() },
|
|
729
|
+
systemCache: { clear: vi.fn() },
|
|
730
|
+
localSchemaCache: { get: vi.fn(), set: vi.fn() },
|
|
731
|
+
lockCache: undefined,
|
|
732
|
+
} as any);
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### Spying on Service Methods
|
|
736
|
+
|
|
737
|
+
Use `vi.spyOn` to override specific methods while keeping the rest of the service intact:
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
import { ItemsService } from './items.js';
|
|
741
|
+
|
|
742
|
+
const createOneSpy = vi.spyOn(ItemsService.prototype, 'createOne').mockResolvedValue('new-id');
|
|
743
|
+
|
|
744
|
+
// Test your code
|
|
745
|
+
expect(createOneSpy).toHaveBeenCalledWith(expect.objectContaining({ field: 'value' }));
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## Contributing
|
|
751
|
+
|
|
752
|
+
When adding new mock utilities:
|
|
753
|
+
|
|
754
|
+
1. Create or update the appropriate mock file
|
|
755
|
+
2. Add comprehensive JSDoc comments with examples
|
|
756
|
+
3. Document the utility in this README
|
|
757
|
+
4. Export all functions from the module
|
|
758
|
+
5. Use TypeScript for proper typing
|
|
759
|
+
6. Include return type documentation
|
|
760
|
+
7. Test the mock with actual service tests
|