@dpesch/mantisbt-mcp-server 1.10.2 → 1.10.6
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/CHANGELOG.md +34 -0
- package/README.de.md +2 -1
- package/README.md +2 -1
- package/dist/client.js +0 -15
- package/dist/tools/files.js +14 -15
- package/docs/cookbook.de.md +1 -1
- package/docs/cookbook.md +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
- package/.gitea/workflows/ci.yml +0 -95
- package/.github/workflows/ci.yml +0 -32
- package/scripts/hooks/pre-push.mjs +0 -220
- package/scripts/init.mjs +0 -95
- package/scripts/record-fixtures.ts +0 -160
- package/tests/cache.test.ts +0 -265
- package/tests/client.test.ts +0 -372
- package/tests/config.test.ts +0 -263
- package/tests/fixtures/get_current_user.json +0 -40
- package/tests/fixtures/get_issue.json +0 -133
- package/tests/fixtures/get_issue_enums.json +0 -67
- package/tests/fixtures/get_issue_fields_sample.json +0 -76
- package/tests/fixtures/get_project_categories.json +0 -157
- package/tests/fixtures/get_project_versions.json +0 -3
- package/tests/fixtures/get_project_versions_with_data.json +0 -52
- package/tests/fixtures/list_issues.json +0 -95
- package/tests/fixtures/list_projects.json +0 -65
- package/tests/helpers/mock-server.ts +0 -166
- package/tests/helpers/search-mocks.ts +0 -84
- package/tests/prompts/prompts.test.ts +0 -242
- package/tests/resources/resources.test.ts +0 -309
- package/tests/search/embedder.test.ts +0 -81
- package/tests/search/highlight.test.ts +0 -129
- package/tests/search/store.test.ts +0 -193
- package/tests/search/sync.test.ts +0 -249
- package/tests/search/tools.test.ts +0 -661
- package/tests/tools/config.test.ts +0 -212
- package/tests/tools/files.test.ts +0 -344
- package/tests/tools/issues.test.ts +0 -1180
- package/tests/tools/metadata.test.ts +0 -509
- package/tests/tools/monitors.test.ts +0 -101
- package/tests/tools/projects.test.ts +0 -338
- package/tests/tools/relationships.test.ts +0 -177
- package/tests/tools/string-coercion.test.ts +0 -317
- package/tests/tools/users.test.ts +0 -62
- package/tests/utils/date-filter.test.ts +0 -169
- package/tests/version-hint.test.ts +0 -230
- package/tsconfig.build.json +0 -8
- package/vitest.config.ts +0 -8
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for string-to-number coercion in MCP tool schemas.
|
|
3
|
-
*
|
|
4
|
-
* The MCP protocol allows clients to pass numeric IDs as strings (e.g. "1940"
|
|
5
|
-
* instead of 1940). Without z.coerce.number(), the Zod schema would reject
|
|
6
|
-
* these inputs with error -32602 (Invalid params).
|
|
7
|
-
*
|
|
8
|
-
* These tests use { validate: true } to run args through the Zod schema —
|
|
9
|
-
* exactly as the real MCP server does — before passing them to the handler.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
13
|
-
import { MantisClient } from '../../src/client.js';
|
|
14
|
-
import { registerIssueTools } from '../../src/tools/issues.js';
|
|
15
|
-
import { MetadataCache } from '../../src/cache.js';
|
|
16
|
-
import { registerNoteTools } from '../../src/tools/notes.js';
|
|
17
|
-
import { registerFileTools } from '../../src/tools/files.js';
|
|
18
|
-
import { registerMonitorTools } from '../../src/tools/monitors.js';
|
|
19
|
-
import { registerRelationshipTools } from '../../src/tools/relationships.js';
|
|
20
|
-
import { registerTagTools } from '../../src/tools/tags.js';
|
|
21
|
-
import { registerProjectTools } from '../../src/tools/projects.js';
|
|
22
|
-
import { registerVersionTools } from '../../src/tools/version.js';
|
|
23
|
-
import { VersionHintService } from '../../src/version-hint.js';
|
|
24
|
-
import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Setup
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
let mockServer: MockMcpServer;
|
|
31
|
-
let client: MantisClient;
|
|
32
|
-
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
mockServer = new MockMcpServer();
|
|
35
|
-
client = new MantisClient('https://mantis.example.com', 'test-token');
|
|
36
|
-
const stubCache = { loadIfValid: vi.fn(async () => null) } as unknown as MetadataCache;
|
|
37
|
-
registerIssueTools(mockServer as never, client, stubCache);
|
|
38
|
-
registerNoteTools(mockServer as never, client);
|
|
39
|
-
registerFileTools(mockServer as never, client);
|
|
40
|
-
registerMonitorTools(mockServer as never, client);
|
|
41
|
-
registerRelationshipTools(mockServer as never, client);
|
|
42
|
-
registerTagTools(mockServer as never, client);
|
|
43
|
-
registerProjectTools(mockServer as never, client);
|
|
44
|
-
registerVersionTools(mockServer as never, client, new VersionHintService(), '0.0.0-test');
|
|
45
|
-
vi.stubGlobal('fetch', vi.fn());
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
afterEach(() => {
|
|
49
|
-
vi.unstubAllGlobals();
|
|
50
|
-
vi.clearAllMocks();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Issues
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
describe('string-coercion – get_issue', () => {
|
|
58
|
-
it('accepts issue_id as string "1940"', async () => {
|
|
59
|
-
vi.mocked(fetch).mockResolvedValue(
|
|
60
|
-
makeResponse(200, JSON.stringify({ issues: [{ id: 1940, summary: 'Test' }] }))
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
const result = await mockServer.callTool('get_issue', { id: '1940' }, { validate: true });
|
|
64
|
-
|
|
65
|
-
expect(result.isError).toBeUndefined();
|
|
66
|
-
const calledUrl = vi.mocked(fetch).mock.calls[0]![0] as string;
|
|
67
|
-
expect(calledUrl).toContain('1940');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('rejects a non-numeric string', async () => {
|
|
71
|
-
const result = await mockServer.callTool('get_issue', { id: 'abc' }, { validate: true });
|
|
72
|
-
expect(result.isError).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('string-coercion – list_issues', () => {
|
|
77
|
-
it('accepts project_id as string', async () => {
|
|
78
|
-
vi.mocked(fetch).mockResolvedValue(
|
|
79
|
-
makeResponse(200, JSON.stringify({ issues: [], total_count: 0 }))
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const result = await mockServer.callTool(
|
|
83
|
-
'list_issues',
|
|
84
|
-
{ project_id: '5', page: '1', page_size: '10' },
|
|
85
|
-
{ validate: true },
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
expect(result.isError).toBeUndefined();
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('string-coercion – update_issue', () => {
|
|
93
|
-
it('accepts id as string', async () => {
|
|
94
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issue: { id: 99, summary: 'Updated' } })));
|
|
95
|
-
|
|
96
|
-
const result = await mockServer.callTool(
|
|
97
|
-
'update_issue',
|
|
98
|
-
{ id: '99', fields: { summary: 'Updated' } },
|
|
99
|
-
{ validate: true },
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
expect(result.isError).toBeUndefined();
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('string-coercion – delete_issue', () => {
|
|
107
|
-
it('accepts id as string', async () => {
|
|
108
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
|
|
109
|
-
|
|
110
|
-
const result = await mockServer.callTool('delete_issue', { id: '42' }, { validate: true });
|
|
111
|
-
|
|
112
|
-
expect(result.isError).toBeUndefined();
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
// Notes
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
|
|
120
|
-
describe('string-coercion – list_notes', () => {
|
|
121
|
-
it('accepts issue_id as string', async () => {
|
|
122
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issues: [{ notes: [] }] })));
|
|
123
|
-
|
|
124
|
-
const result = await mockServer.callTool('list_notes', { issue_id: '1940' }, { validate: true });
|
|
125
|
-
|
|
126
|
-
expect(result.isError).toBeUndefined();
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('string-coercion – delete_note', () => {
|
|
131
|
-
it('accepts issue_id and note_id as strings', async () => {
|
|
132
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
|
|
133
|
-
|
|
134
|
-
const result = await mockServer.callTool(
|
|
135
|
-
'delete_note',
|
|
136
|
-
{ issue_id: '1940', note_id: '77' },
|
|
137
|
-
{ validate: true },
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
expect(result.isError).toBeUndefined();
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
// Files
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
|
|
148
|
-
describe('string-coercion – list_issue_files', () => {
|
|
149
|
-
it('accepts issue_id as string', async () => {
|
|
150
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issues: [{ attachments: [] }] })));
|
|
151
|
-
|
|
152
|
-
const result = await mockServer.callTool('list_issue_files', { issue_id: '1940' }, { validate: true });
|
|
153
|
-
|
|
154
|
-
expect(result.isError).toBeUndefined();
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
// Monitors
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
|
|
162
|
-
describe('string-coercion – add_monitor', () => {
|
|
163
|
-
it('accepts issue_id as string', async () => {
|
|
164
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, ''));
|
|
165
|
-
|
|
166
|
-
const result = await mockServer.callTool(
|
|
167
|
-
'add_monitor',
|
|
168
|
-
{ issue_id: '1940', username: 'jdoe' },
|
|
169
|
-
{ validate: true },
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
expect(result.isError).toBeUndefined();
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
describe('string-coercion – remove_monitor', () => {
|
|
177
|
-
it('accepts issue_id as string', async () => {
|
|
178
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
|
|
179
|
-
|
|
180
|
-
const result = await mockServer.callTool(
|
|
181
|
-
'remove_monitor',
|
|
182
|
-
{ issue_id: '1940', username: 'jdoe' },
|
|
183
|
-
{ validate: true },
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
expect(result.isError).toBeUndefined();
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
// Relationships
|
|
192
|
-
// ---------------------------------------------------------------------------
|
|
193
|
-
|
|
194
|
-
describe('string-coercion – add_relationship', () => {
|
|
195
|
-
it('accepts issue_id, target_id and type_id as strings', async () => {
|
|
196
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ id: 1 })));
|
|
197
|
-
|
|
198
|
-
const result = await mockServer.callTool(
|
|
199
|
-
'add_relationship',
|
|
200
|
-
{ issue_id: '1940', target_id: '1941', type_id: '1' },
|
|
201
|
-
{ validate: true },
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
expect(result.isError).toBeUndefined();
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
describe('string-coercion – remove_relationship', () => {
|
|
209
|
-
it('accepts issue_id and relationship_id as strings', async () => {
|
|
210
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
|
|
211
|
-
|
|
212
|
-
const result = await mockServer.callTool(
|
|
213
|
-
'remove_relationship',
|
|
214
|
-
{ issue_id: '1940', relationship_id: '55' },
|
|
215
|
-
{ validate: true },
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
expect(result.isError).toBeUndefined();
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
// Tags
|
|
224
|
-
// ---------------------------------------------------------------------------
|
|
225
|
-
|
|
226
|
-
describe('string-coercion – detach_tag', () => {
|
|
227
|
-
it('accepts issue_id and tag_id as strings', async () => {
|
|
228
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(204, ''));
|
|
229
|
-
|
|
230
|
-
const result = await mockServer.callTool(
|
|
231
|
-
'detach_tag',
|
|
232
|
-
{ issue_id: '1940', tag_id: '7' },
|
|
233
|
-
{ validate: true },
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
expect(result.isError).toBeUndefined();
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
// Projects
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
describe('string-coercion – get_project_versions', () => {
|
|
245
|
-
it('accepts project_id as string', async () => {
|
|
246
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: [{ versions: [] }] })));
|
|
247
|
-
|
|
248
|
-
const result = await mockServer.callTool(
|
|
249
|
-
'get_project_versions',
|
|
250
|
-
{ project_id: '3' },
|
|
251
|
-
{ validate: true },
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
expect(result.isError).toBeUndefined();
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
// Boolean-Parameter als String
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
|
|
262
|
-
describe('string-coercion – update_issue fields as JSON string', () => {
|
|
263
|
-
it('accepts fields as JSON string', async () => {
|
|
264
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ issue: { id: 99, summary: 'Updated' } })));
|
|
265
|
-
const result = await mockServer.callTool(
|
|
266
|
-
'update_issue',
|
|
267
|
-
{ id: 99, fields: '{"summary":"Updated"}' },
|
|
268
|
-
{ validate: true },
|
|
269
|
-
);
|
|
270
|
-
expect(result.isError).toBeUndefined();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('rejects invalid JSON in fields with a validation error (not an uncaught exception)', async () => {
|
|
274
|
-
const result = await mockServer.callTool(
|
|
275
|
-
'update_issue',
|
|
276
|
-
{ id: 99, fields: '{invalid json' },
|
|
277
|
-
{ validate: true },
|
|
278
|
-
);
|
|
279
|
-
expect(result.isError).toBe(true);
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe('string-coercion – update_issue dry_run as string', () => {
|
|
284
|
-
it('accepts dry_run "true" as boolean true', async () => {
|
|
285
|
-
const result = await mockServer.callTool(
|
|
286
|
-
'update_issue',
|
|
287
|
-
{ id: 99, fields: { summary: 'x' }, dry_run: 'true' },
|
|
288
|
-
{ validate: true },
|
|
289
|
-
);
|
|
290
|
-
expect(result.isError).toBeUndefined();
|
|
291
|
-
expect(result.content[0]?.text).toContain('"dry_run": true');
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
describe('string-coercion – get_project_versions boolean flags', () => {
|
|
296
|
-
it('accepts obsolete "true" and inherit "false" as booleans', async () => {
|
|
297
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ projects: [{ versions: [] }] })));
|
|
298
|
-
const result = await mockServer.callTool(
|
|
299
|
-
'get_project_versions',
|
|
300
|
-
{ project_id: 3, obsolete: 'true', inherit: 'false' },
|
|
301
|
-
{ validate: true },
|
|
302
|
-
);
|
|
303
|
-
expect(result.isError).toBeUndefined();
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
describe('string-coercion – get_mantis_version check_latest as string', () => {
|
|
308
|
-
it('accepts check_latest "false" as boolean false', async () => {
|
|
309
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify({ version: '2.26.0' })));
|
|
310
|
-
const result = await mockServer.callTool(
|
|
311
|
-
'get_mantis_version',
|
|
312
|
-
{ check_latest: 'false' },
|
|
313
|
-
{ validate: true },
|
|
314
|
-
);
|
|
315
|
-
expect(result.isError).toBeUndefined();
|
|
316
|
-
});
|
|
317
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
-
import { join, dirname } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { MantisClient } from '../../src/client.js';
|
|
6
|
-
import { registerUserTools } from '../../src/tools/users.js';
|
|
7
|
-
import { MockMcpServer, makeResponse } from '../helpers/mock-server.js';
|
|
8
|
-
|
|
9
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
-
const __dirname = dirname(__filename);
|
|
11
|
-
const fixturesDir = join(__dirname, '..', 'fixtures');
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Fixtures laden (mit Inline-Fallback)
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
const getCurrentUserFixturePath = join(fixturesDir, 'get_current_user.json');
|
|
18
|
-
|
|
19
|
-
const getCurrentUserFixture = existsSync(getCurrentUserFixturePath)
|
|
20
|
-
? (JSON.parse(readFileSync(getCurrentUserFixturePath, 'utf-8')) as { id: number; name: string; real_name?: string; email?: string })
|
|
21
|
-
: { id: 5, name: 'testuser', real_name: 'Test User', email: 'test@example.com' };
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Setup
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
let mockServer: MockMcpServer;
|
|
28
|
-
let client: MantisClient;
|
|
29
|
-
|
|
30
|
-
beforeEach(() => {
|
|
31
|
-
mockServer = new MockMcpServer();
|
|
32
|
-
client = new MantisClient('https://mantis.example.com', 'test-token');
|
|
33
|
-
registerUserTools(mockServer as never, client);
|
|
34
|
-
vi.stubGlobal('fetch', vi.fn());
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
afterEach(() => {
|
|
38
|
-
vi.unstubAllGlobals();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// get_current_user
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
describe('get_current_user', () => {
|
|
46
|
-
it('ist registriert', () => {
|
|
47
|
-
expect(mockServer.hasToolRegistered('get_current_user')).toBe(true);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('gibt User-Daten zurück mit id und name', async () => {
|
|
51
|
-
vi.mocked(fetch).mockResolvedValue(makeResponse(200, JSON.stringify(getCurrentUserFixture)));
|
|
52
|
-
|
|
53
|
-
const result = await mockServer.callTool('get_current_user', {});
|
|
54
|
-
|
|
55
|
-
expect(result.isError).toBeUndefined();
|
|
56
|
-
const parsed = JSON.parse(result.content[0]!.text) as { id: number; name: string };
|
|
57
|
-
expect(typeof parsed.id).toBe('number');
|
|
58
|
-
expect(typeof parsed.name).toBe('string');
|
|
59
|
-
expect(parsed.id).toBe(getCurrentUserFixture.id);
|
|
60
|
-
expect(parsed.name).toBe(getCurrentUserFixture.name);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { matchesDateFilter } from '../../src/date-filter.js';
|
|
3
|
-
|
|
4
|
-
describe('matchesDateFilter', () => {
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// No filter → always pass
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
it('returns true when no filter is set', () => {
|
|
11
|
-
expect(matchesDateFilter(
|
|
12
|
-
{ updated_at: '2026-03-20T10:00:00Z', created_at: '2026-03-01T00:00:00Z' },
|
|
13
|
-
{}
|
|
14
|
-
)).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('returns true for an item with no dates when no filter is set', () => {
|
|
18
|
-
expect(matchesDateFilter({}, {})).toBe(true);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// updated_after
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
it('updated_after: passes when updated_at is after the threshold', () => {
|
|
26
|
-
expect(matchesDateFilter(
|
|
27
|
-
{ updated_at: '2026-03-25T12:00:00Z' },
|
|
28
|
-
{ updated_after: '2026-03-24T00:00:00Z' }
|
|
29
|
-
)).toBe(true);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('updated_after: fails when updated_at is before the threshold', () => {
|
|
33
|
-
expect(matchesDateFilter(
|
|
34
|
-
{ updated_at: '2026-03-23T12:00:00Z' },
|
|
35
|
-
{ updated_after: '2026-03-24T00:00:00Z' }
|
|
36
|
-
)).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('updated_after: fails when updated_at equals the threshold (exclusive)', () => {
|
|
40
|
-
expect(matchesDateFilter(
|
|
41
|
-
{ updated_at: '2026-03-24T00:00:00Z' },
|
|
42
|
-
{ updated_after: '2026-03-24T00:00:00Z' }
|
|
43
|
-
)).toBe(false);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('updated_after: fails when updated_at is missing', () => {
|
|
47
|
-
expect(matchesDateFilter(
|
|
48
|
-
{ created_at: '2026-03-25T00:00:00Z' },
|
|
49
|
-
{ updated_after: '2026-03-24T00:00:00Z' }
|
|
50
|
-
)).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// updated_before
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
it('updated_before: passes when updated_at is before the threshold', () => {
|
|
58
|
-
expect(matchesDateFilter(
|
|
59
|
-
{ updated_at: '2026-03-20T00:00:00Z' },
|
|
60
|
-
{ updated_before: '2026-03-25T00:00:00Z' }
|
|
61
|
-
)).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('updated_before: fails when updated_at is after the threshold', () => {
|
|
65
|
-
expect(matchesDateFilter(
|
|
66
|
-
{ updated_at: '2026-03-26T00:00:00Z' },
|
|
67
|
-
{ updated_before: '2026-03-25T00:00:00Z' }
|
|
68
|
-
)).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('updated_before: fails when updated_at equals the threshold (exclusive)', () => {
|
|
72
|
-
expect(matchesDateFilter(
|
|
73
|
-
{ updated_at: '2026-03-25T00:00:00Z' },
|
|
74
|
-
{ updated_before: '2026-03-25T00:00:00Z' }
|
|
75
|
-
)).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('updated_before: fails when updated_at is missing', () => {
|
|
79
|
-
expect(matchesDateFilter(
|
|
80
|
-
{},
|
|
81
|
-
{ updated_before: '2026-03-25T00:00:00Z' }
|
|
82
|
-
)).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// created_after
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
it('created_after: passes when created_at is after the threshold', () => {
|
|
90
|
-
expect(matchesDateFilter(
|
|
91
|
-
{ created_at: '2026-03-25T12:00:00Z' },
|
|
92
|
-
{ created_after: '2026-03-24T00:00:00Z' }
|
|
93
|
-
)).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('created_after: fails when created_at is before the threshold', () => {
|
|
97
|
-
expect(matchesDateFilter(
|
|
98
|
-
{ created_at: '2026-03-23T00:00:00Z' },
|
|
99
|
-
{ created_after: '2026-03-24T00:00:00Z' }
|
|
100
|
-
)).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('created_after: fails when created_at is missing', () => {
|
|
104
|
-
expect(matchesDateFilter(
|
|
105
|
-
{ updated_at: '2026-03-25T00:00:00Z' },
|
|
106
|
-
{ created_after: '2026-03-24T00:00:00Z' }
|
|
107
|
-
)).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
// created_before
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
it('created_before: passes when created_at is before the threshold', () => {
|
|
115
|
-
expect(matchesDateFilter(
|
|
116
|
-
{ created_at: '2026-03-20T00:00:00Z' },
|
|
117
|
-
{ created_before: '2026-03-25T00:00:00Z' }
|
|
118
|
-
)).toBe(true);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('created_before: fails when created_at is after the threshold', () => {
|
|
122
|
-
expect(matchesDateFilter(
|
|
123
|
-
{ created_at: '2026-03-26T00:00:00Z' },
|
|
124
|
-
{ created_before: '2026-03-25T00:00:00Z' }
|
|
125
|
-
)).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
// ---------------------------------------------------------------------------
|
|
129
|
-
// Combined filters (time window)
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
it('updated_after + updated_before: passes when updated_at is within window', () => {
|
|
133
|
-
expect(matchesDateFilter(
|
|
134
|
-
{ updated_at: '2026-03-22T00:00:00Z' },
|
|
135
|
-
{ updated_after: '2026-03-20T00:00:00Z', updated_before: '2026-03-25T00:00:00Z' }
|
|
136
|
-
)).toBe(true);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('updated_after + updated_before: fails when updated_at is outside window', () => {
|
|
140
|
-
expect(matchesDateFilter(
|
|
141
|
-
{ updated_at: '2026-03-26T00:00:00Z' },
|
|
142
|
-
{ updated_after: '2026-03-20T00:00:00Z', updated_before: '2026-03-25T00:00:00Z' }
|
|
143
|
-
)).toBe(false);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('all four filters: passes only when both dates are within their windows', () => {
|
|
147
|
-
expect(matchesDateFilter(
|
|
148
|
-
{ updated_at: '2026-03-22T00:00:00Z', created_at: '2026-03-10T00:00:00Z' },
|
|
149
|
-
{
|
|
150
|
-
updated_after: '2026-03-20T00:00:00Z',
|
|
151
|
-
updated_before: '2026-03-25T00:00:00Z',
|
|
152
|
-
created_after: '2026-03-05T00:00:00Z',
|
|
153
|
-
created_before: '2026-03-15T00:00:00Z',
|
|
154
|
-
}
|
|
155
|
-
)).toBe(true);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('all four filters: fails when one date is out of range', () => {
|
|
159
|
-
expect(matchesDateFilter(
|
|
160
|
-
{ updated_at: '2026-03-22T00:00:00Z', created_at: '2026-03-16T00:00:00Z' }, // created_at too late
|
|
161
|
-
{
|
|
162
|
-
updated_after: '2026-03-20T00:00:00Z',
|
|
163
|
-
updated_before: '2026-03-25T00:00:00Z',
|
|
164
|
-
created_after: '2026-03-05T00:00:00Z',
|
|
165
|
-
created_before: '2026-03-15T00:00:00Z',
|
|
166
|
-
}
|
|
167
|
-
)).toBe(false);
|
|
168
|
-
});
|
|
169
|
-
});
|