@objectstack/rest 4.0.4 → 4.1.0

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/src/rest.test.ts DELETED
@@ -1,672 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, beforeEach, vi } from 'vitest';
4
- import { RouteManager, RouteGroupBuilder } from './route-manager';
5
- import { RestServer } from './rest-server';
6
- import { createRestApiPlugin } from './rest-api-plugin';
7
- import type { RestApiPluginConfig } from './rest-api-plugin';
8
-
9
- // ---------------------------------------------------------------------------
10
- // Mocks & Helpers
11
- // ---------------------------------------------------------------------------
12
-
13
- /** Minimal IHttpServer mock */
14
- function createMockServer() {
15
- return {
16
- get: vi.fn(),
17
- post: vi.fn(),
18
- put: vi.fn(),
19
- delete: vi.fn(),
20
- patch: vi.fn(),
21
- use: vi.fn(),
22
- listen: vi.fn().mockResolvedValue(undefined),
23
- close: vi.fn().mockResolvedValue(undefined),
24
- };
25
- }
26
-
27
- /** Minimal ObjectStackProtocol mock */
28
- function createMockProtocol() {
29
- return {
30
- getDiscovery: vi.fn().mockResolvedValue({
31
- version: 'v0',
32
- endpoints: { data: '', metadata: '', ui: '', auth: '/auth' },
33
- }),
34
- getMetaTypes: vi.fn().mockResolvedValue([]),
35
- getMetaItems: vi.fn().mockResolvedValue([]),
36
- getMetaItem: vi.fn().mockResolvedValue({}),
37
- getMetaItemCached: undefined as any,
38
- saveMetaItem: undefined as any,
39
- getUiView: undefined as any,
40
- findData: vi.fn().mockResolvedValue([]),
41
- getData: vi.fn().mockResolvedValue({}),
42
- createData: vi.fn().mockResolvedValue({ id: '1' }),
43
- updateData: vi.fn().mockResolvedValue({}),
44
- deleteData: vi.fn().mockResolvedValue({ success: true }),
45
- batchData: undefined as any,
46
- createManyData: undefined as any,
47
- updateManyData: undefined as any,
48
- deleteManyData: undefined as any,
49
- };
50
- }
51
-
52
- /** Minimal PluginContext mock */
53
- function createMockPluginContext(services: Record<string, any> = {}) {
54
- return {
55
- registerService: vi.fn(),
56
- getService: vi.fn((name: string) => {
57
- if (services[name]) return services[name];
58
- throw new Error(`Service '${name}' not found`);
59
- }),
60
- getServices: vi.fn(() => new Map(Object.entries(services))),
61
- hook: vi.fn(),
62
- trigger: vi.fn().mockResolvedValue(undefined),
63
- logger: {
64
- debug: vi.fn(),
65
- info: vi.fn(),
66
- warn: vi.fn(),
67
- error: vi.fn(),
68
- },
69
- getKernel: vi.fn(),
70
- };
71
- }
72
-
73
- /** Dummy handler */
74
- const noop = vi.fn();
75
-
76
- // ---------------------------------------------------------------------------
77
- // RouteManager
78
- // ---------------------------------------------------------------------------
79
-
80
- describe('RouteManager', () => {
81
- let server: ReturnType<typeof createMockServer>;
82
- let manager: RouteManager;
83
-
84
- beforeEach(() => {
85
- server = createMockServer();
86
- manager = new RouteManager(server as any);
87
- });
88
-
89
- // -- Registration --------------------------------------------------------
90
-
91
- describe('register', () => {
92
- it('should register a GET route and delegate to server.get', () => {
93
- manager.register({ method: 'GET', path: '/users', handler: noop });
94
- expect(server.get).toHaveBeenCalledWith('/users', noop);
95
- expect(manager.count()).toBe(1);
96
- });
97
-
98
- it('should register POST, PUT, PATCH, DELETE routes', () => {
99
- manager.register({ method: 'POST', path: '/a', handler: noop });
100
- manager.register({ method: 'PUT', path: '/b', handler: noop });
101
- manager.register({ method: 'PATCH', path: '/c', handler: noop });
102
- manager.register({ method: 'DELETE', path: '/d', handler: noop });
103
-
104
- expect(server.post).toHaveBeenCalledWith('/a', noop);
105
- expect(server.put).toHaveBeenCalledWith('/b', noop);
106
- expect(server.patch).toHaveBeenCalledWith('/c', noop);
107
- expect(server.delete).toHaveBeenCalledWith('/d', noop);
108
- expect(manager.count()).toBe(4);
109
- });
110
-
111
- it('should store metadata on the route entry', () => {
112
- manager.register({
113
- method: 'GET',
114
- path: '/items',
115
- handler: noop,
116
- metadata: { summary: 'List items', tags: ['items'] },
117
- });
118
-
119
- const entry = manager.get('GET', '/items');
120
- expect(entry).toBeDefined();
121
- expect(entry!.metadata?.summary).toBe('List items');
122
- expect(entry!.metadata?.tags).toContain('items');
123
- });
124
-
125
- it('should throw when a string handler is provided', () => {
126
- expect(() =>
127
- manager.register({ method: 'GET', path: '/x', handler: 'someHandler' }),
128
- ).toThrow(/String-based route handlers/);
129
- });
130
- });
131
-
132
- // -- registerMany --------------------------------------------------------
133
-
134
- describe('registerMany', () => {
135
- it('should register multiple routes at once', () => {
136
- manager.registerMany([
137
- { method: 'GET', path: '/a', handler: noop },
138
- { method: 'POST', path: '/b', handler: noop },
139
- ]);
140
- expect(manager.count()).toBe(2);
141
- });
142
- });
143
-
144
- // -- Lookup / query -------------------------------------------------------
145
-
146
- describe('get', () => {
147
- it('should return undefined for unregistered route', () => {
148
- expect(manager.get('GET', '/nothing')).toBeUndefined();
149
- });
150
-
151
- it('should return the entry for a registered route', () => {
152
- manager.register({ method: 'GET', path: '/users', handler: noop });
153
- const entry = manager.get('GET', '/users');
154
- expect(entry).toBeDefined();
155
- expect(entry!.path).toBe('/users');
156
- });
157
- });
158
-
159
- describe('getAll', () => {
160
- it('should return all registered routes', () => {
161
- manager.register({ method: 'GET', path: '/a', handler: noop });
162
- manager.register({ method: 'POST', path: '/b', handler: noop });
163
- expect(manager.getAll()).toHaveLength(2);
164
- });
165
- });
166
-
167
- describe('getByMethod', () => {
168
- it('should filter routes by HTTP method', () => {
169
- manager.register({ method: 'GET', path: '/a', handler: noop });
170
- manager.register({ method: 'GET', path: '/b', handler: noop });
171
- manager.register({ method: 'POST', path: '/c', handler: noop });
172
-
173
- expect(manager.getByMethod('GET')).toHaveLength(2);
174
- expect(manager.getByMethod('POST')).toHaveLength(1);
175
- expect(manager.getByMethod('DELETE')).toHaveLength(0);
176
- });
177
- });
178
-
179
- describe('getByPrefix', () => {
180
- it('should filter routes by path prefix', () => {
181
- manager.register({ method: 'GET', path: '/api/users', handler: noop });
182
- manager.register({ method: 'GET', path: '/api/items', handler: noop });
183
- manager.register({ method: 'GET', path: '/other', handler: noop });
184
-
185
- expect(manager.getByPrefix('/api')).toHaveLength(2);
186
- });
187
- });
188
-
189
- describe('getByTag', () => {
190
- it('should filter routes by metadata tag', () => {
191
- manager.register({
192
- method: 'GET', path: '/a', handler: noop,
193
- metadata: { tags: ['users'] },
194
- });
195
- manager.register({
196
- method: 'GET', path: '/b', handler: noop,
197
- metadata: { tags: ['items'] },
198
- });
199
- manager.register({ method: 'GET', path: '/c', handler: noop });
200
-
201
- expect(manager.getByTag('users')).toHaveLength(1);
202
- expect(manager.getByTag('missing')).toHaveLength(0);
203
- });
204
- });
205
-
206
- // -- Unregister -----------------------------------------------------------
207
-
208
- describe('unregister', () => {
209
- it('should remove a route from the registry', () => {
210
- manager.register({ method: 'GET', path: '/x', handler: noop });
211
- expect(manager.count()).toBe(1);
212
-
213
- manager.unregister('GET', '/x');
214
- expect(manager.count()).toBe(0);
215
- expect(manager.get('GET', '/x')).toBeUndefined();
216
- });
217
- });
218
-
219
- // -- Clear ----------------------------------------------------------------
220
-
221
- describe('clear', () => {
222
- it('should remove all routes', () => {
223
- manager.registerMany([
224
- { method: 'GET', path: '/a', handler: noop },
225
- { method: 'POST', path: '/b', handler: noop },
226
- ]);
227
- manager.clear();
228
- expect(manager.count()).toBe(0);
229
- });
230
- });
231
-
232
- // -- Group ----------------------------------------------------------------
233
-
234
- describe('group', () => {
235
- it('should create routes with the prefix prepended', () => {
236
- manager.group('/api/v1', (group) => {
237
- group.get('/users', noop);
238
- group.post('/users', noop);
239
- group.put('/users/:id', noop);
240
- group.patch('/users/:id', noop);
241
- group.delete('/users/:id', noop);
242
- });
243
-
244
- expect(manager.count()).toBe(5);
245
- expect(manager.get('GET', '/api/v1/users')).toBeDefined();
246
- expect(manager.get('POST', '/api/v1/users')).toBeDefined();
247
- expect(manager.get('PUT', '/api/v1/users/:id')).toBeDefined();
248
- expect(manager.get('PATCH', '/api/v1/users/:id')).toBeDefined();
249
- expect(manager.get('DELETE', '/api/v1/users/:id')).toBeDefined();
250
- });
251
-
252
- it('should normalize paths (strip trailing slash on prefix, ensure leading slash on path)', () => {
253
- manager.group('/api/', (group) => {
254
- group.get('items', noop);
255
- });
256
- expect(manager.get('GET', '/api/items')).toBeDefined();
257
- });
258
-
259
- it('should allow chaining on group builder methods', () => {
260
- manager.group('/api', (group) => {
261
- const result = group
262
- .get('/a', noop)
263
- .post('/b', noop);
264
- expect(result).toBe(group);
265
- });
266
- });
267
- });
268
- });
269
-
270
- // ---------------------------------------------------------------------------
271
- // RestServer
272
- // ---------------------------------------------------------------------------
273
-
274
- describe('RestServer', () => {
275
- let server: ReturnType<typeof createMockServer>;
276
- let protocol: ReturnType<typeof createMockProtocol>;
277
-
278
- beforeEach(() => {
279
- server = createMockServer();
280
- protocol = createMockProtocol();
281
- });
282
-
283
- // -- Constructor & defaults -----------------------------------------------
284
-
285
- describe('constructor', () => {
286
- it('should create a RestServer with default config', () => {
287
- const rest = new RestServer(server as any, protocol as any);
288
- expect(rest).toBeDefined();
289
- expect(rest.getRouteManager()).toBeInstanceOf(RouteManager);
290
- });
291
-
292
- it('should accept custom config', () => {
293
- const rest = new RestServer(server as any, protocol as any, {
294
- api: { version: 'v2', basePath: '/custom' },
295
- } as any);
296
- expect(rest).toBeDefined();
297
- });
298
- });
299
-
300
- // -- registerRoutes -------------------------------------------------------
301
-
302
- describe('registerRoutes', () => {
303
- it('should register discovery, metadata, UI, CRUD, and batch routes by default', () => {
304
- const rest = new RestServer(server as any, protocol as any);
305
- rest.registerRoutes();
306
-
307
- const routes = rest.getRoutes();
308
- expect(routes.length).toBeGreaterThan(0);
309
-
310
- // Expect at least discovery + metadata + CRUD routes
311
- const paths = routes.map((r) => r.path);
312
- // Discovery (both basePath and basePath/discovery)
313
- expect(paths).toContain('/api/v1');
314
- expect(paths).toContain('/api/v1/discovery');
315
- // Metadata
316
- expect(paths.some((p) => p.includes('/meta'))).toBe(true);
317
- // CRUD
318
- expect(paths.some((p) => p.includes('/data'))).toBe(true);
319
- });
320
-
321
- it('should use custom apiPath when specified', () => {
322
- const rest = new RestServer(server as any, protocol as any, {
323
- api: { apiPath: '/custom/path' },
324
- } as any);
325
- rest.registerRoutes();
326
-
327
- const paths = rest.getRoutes().map((r) => r.path);
328
- expect(paths.some((p) => p.startsWith('/custom/path'))).toBe(true);
329
- });
330
-
331
- it('should skip CRUD routes when enableCrud is false', () => {
332
- const rest = new RestServer(server as any, protocol as any, {
333
- api: { enableCrud: false },
334
- } as any);
335
- rest.registerRoutes();
336
-
337
- const tags = rest.getRoutes().flatMap((r) => r.metadata?.tags ?? []);
338
- expect(tags).not.toContain('crud');
339
- });
340
-
341
- it('should skip metadata routes when enableMetadata is false', () => {
342
- const rest = new RestServer(server as any, protocol as any, {
343
- api: { enableMetadata: false },
344
- } as any);
345
- rest.registerRoutes();
346
-
347
- const routes = rest.getRoutes();
348
- // Only the PUT /meta/:type/:name is always registered, but enableMetadata=false
349
- // skips the entire registerMetadataEndpoints call
350
- expect(routes.every((r) => !r.path.includes('/meta'))).toBe(true);
351
- });
352
-
353
- it('should skip discovery when enableDiscovery is false', () => {
354
- const rest = new RestServer(server as any, protocol as any, {
355
- api: { enableDiscovery: false },
356
- } as any);
357
- rest.registerRoutes();
358
-
359
- const routes = rest.getRoutes();
360
- // Neither basePath nor basePath/discovery should be registered
361
- const discoveryRoutes = routes.filter((r) =>
362
- r.metadata?.tags?.includes('discovery'),
363
- );
364
- expect(discoveryRoutes).toHaveLength(0);
365
- });
366
-
367
- it('should skip batch routes when enableBatch is false', () => {
368
- const rest = new RestServer(server as any, protocol as any, {
369
- api: { enableBatch: false },
370
- } as any);
371
- rest.registerRoutes();
372
-
373
- const tags = rest.getRoutes().flatMap((r) => r.metadata?.tags ?? []);
374
- expect(tags).not.toContain('batch');
375
- });
376
-
377
- it('should register batch endpoints when protocol implements batch methods', () => {
378
- protocol.batchData = vi.fn().mockResolvedValue({});
379
- protocol.createManyData = vi.fn().mockResolvedValue([]);
380
- protocol.updateManyData = vi.fn().mockResolvedValue([]);
381
- protocol.deleteManyData = vi.fn().mockResolvedValue([]);
382
-
383
- const rest = new RestServer(server as any, protocol as any);
384
- rest.registerRoutes();
385
-
386
- const batchRoutes = rest.getRoutes().filter((r) =>
387
- r.metadata?.tags?.includes('batch'),
388
- );
389
- expect(batchRoutes.length).toBeGreaterThan(0);
390
- });
391
-
392
- it('should register UI view endpoint when enableUi is true', () => {
393
- const rest = new RestServer(server as any, protocol as any);
394
- rest.registerRoutes();
395
-
396
- const uiRoutes = rest.getRoutes().filter((r) =>
397
- r.metadata?.tags?.includes('ui'),
398
- );
399
- expect(uiRoutes.length).toBeGreaterThan(0);
400
- });
401
-
402
- it('should not register i18n endpoints (i18n routes are self-registered by service-i18n)', () => {
403
- const rest = new RestServer(server as any, protocol as any);
404
- rest.registerRoutes();
405
-
406
- const i18nRoutes = rest.getRoutes().filter((r) =>
407
- r.metadata?.tags?.includes('i18n'),
408
- );
409
- expect(i18nRoutes).toHaveLength(0);
410
- });
411
- });
412
-
413
- // -- getRouteManager / getRoutes ------------------------------------------
414
-
415
- describe('getRouteManager', () => {
416
- it('should return the internal RouteManager instance', () => {
417
- const rest = new RestServer(server as any, protocol as any);
418
- const rm = rest.getRouteManager();
419
- expect(rm).toBeInstanceOf(RouteManager);
420
- });
421
- });
422
-
423
- describe('getRoutes', () => {
424
- it('should return an empty array before registerRoutes is called', () => {
425
- const rest = new RestServer(server as any, protocol as any);
426
- expect(rest.getRoutes()).toEqual([]);
427
- });
428
- });
429
-
430
- describe('getData handler expand/select forwarding', () => {
431
- function getRoute(rest: any, pathSuffix: string) {
432
- const routes = rest.getRoutes();
433
- return routes.find(
434
- (r: any) => r.method === 'GET' && r.path === `/api/v1/data/${pathSuffix}`,
435
- );
436
- }
437
-
438
- it('should pass expand and select query params to protocol.getData', async () => {
439
- const rest = new RestServer(server as any, protocol as any);
440
- rest.registerRoutes();
441
-
442
- const getByIdRoute = getRoute(rest, ':object/:id');
443
- expect(getByIdRoute).toBeDefined();
444
-
445
- // Simulate request with expand and select query params
446
- const mockReq = {
447
- params: { object: 'order_item', id: 'oi_123' },
448
- query: { expand: 'order,product', select: 'name,total' },
449
- };
450
- const mockRes = {
451
- json: vi.fn(),
452
- status: vi.fn().mockReturnThis(),
453
- };
454
-
455
- protocol.getData.mockResolvedValue({
456
- object: 'order_item',
457
- id: 'oi_123',
458
- record: { id: 'oi_123', name: 'Item 1' },
459
- });
460
-
461
- await getByIdRoute!.handler(mockReq, mockRes);
462
-
463
- expect(protocol.getData).toHaveBeenCalledWith(
464
- expect.objectContaining({
465
- object: 'order_item',
466
- id: 'oi_123',
467
- expand: 'order,product',
468
- select: 'name,total',
469
- }),
470
- );
471
- });
472
-
473
- it('should omit expand/select when not present in query', async () => {
474
- const rest = new RestServer(server as any, protocol as any);
475
- rest.registerRoutes();
476
-
477
- const getByIdRoute = getRoute(rest, ':object/:id');
478
-
479
- const mockReq = {
480
- params: { object: 'contact', id: 'c_1' },
481
- query: {},
482
- };
483
- const mockRes = {
484
- json: vi.fn(),
485
- status: vi.fn().mockReturnThis(),
486
- };
487
-
488
- protocol.getData.mockResolvedValue({
489
- object: 'contact',
490
- id: 'c_1',
491
- record: { id: 'c_1' },
492
- });
493
-
494
- await getByIdRoute!.handler(mockReq, mockRes);
495
-
496
- // Should NOT have expand or select keys in the call
497
- const callArg = protocol.getData.mock.calls[protocol.getData.mock.calls.length - 1][0];
498
- expect(callArg).toEqual({ object: 'contact', id: 'c_1' });
499
- });
500
- });
501
-
502
- describe('findData handler expand/populate forwarding', () => {
503
- function getListRoute(rest: any) {
504
- const routes = rest.getRoutes();
505
- return routes.find(
506
- (r: any) => r.method === 'GET' && r.path === '/api/v1/data/:object',
507
- );
508
- }
509
-
510
- it('should pass query params including expand to protocol.findData', async () => {
511
- const rest = new RestServer(server as any, protocol as any);
512
- rest.registerRoutes();
513
-
514
- const listRoute = getListRoute(rest);
515
- expect(listRoute).toBeDefined();
516
-
517
- const mockReq = {
518
- params: { object: 'order_item' },
519
- query: { expand: 'order,product', top: '10' },
520
- };
521
- const mockRes = {
522
- json: vi.fn(),
523
- status: vi.fn().mockReturnThis(),
524
- };
525
-
526
- protocol.findData.mockResolvedValue({
527
- object: 'order_item',
528
- records: [],
529
- total: 0,
530
- });
531
-
532
- await listRoute!.handler(mockReq, mockRes);
533
-
534
- expect(protocol.findData).toHaveBeenCalledWith({
535
- object: 'order_item',
536
- query: { expand: 'order,product', top: '10' },
537
- });
538
- });
539
-
540
- it('should pass populate query param to protocol.findData', async () => {
541
- const rest = new RestServer(server as any, protocol as any);
542
- rest.registerRoutes();
543
-
544
- const listRoute = getListRoute(rest);
545
-
546
- const mockReq = {
547
- params: { object: 'task' },
548
- query: { populate: 'assignee,project' },
549
- };
550
- const mockRes = {
551
- json: vi.fn(),
552
- status: vi.fn().mockReturnThis(),
553
- };
554
-
555
- protocol.findData.mockResolvedValue({
556
- object: 'task',
557
- records: [],
558
- total: 0,
559
- });
560
-
561
- await listRoute!.handler(mockReq, mockRes);
562
-
563
- expect(protocol.findData).toHaveBeenCalledWith({
564
- object: 'task',
565
- query: { populate: 'assignee,project' },
566
- });
567
- });
568
- });
569
- });
570
-
571
- // ---------------------------------------------------------------------------
572
- // createRestApiPlugin
573
- // ---------------------------------------------------------------------------
574
-
575
- describe('createRestApiPlugin', () => {
576
- it('should return a plugin object with name and version', () => {
577
- const plugin = createRestApiPlugin();
578
- expect(plugin.name).toBe('com.objectstack.rest.api');
579
- expect(plugin.version).toBe('1.0.0');
580
- expect(typeof plugin.init).toBe('function');
581
- expect(typeof plugin.start).toBe('function');
582
- });
583
-
584
- it('should accept custom config', () => {
585
- const cfg: RestApiPluginConfig = {
586
- serverServiceName: 'my.server',
587
- protocolServiceName: 'my.protocol',
588
- };
589
- const plugin = createRestApiPlugin(cfg);
590
- expect(plugin.name).toBe('com.objectstack.rest.api');
591
- });
592
-
593
- describe('init', () => {
594
- it('should resolve without error', async () => {
595
- const plugin = createRestApiPlugin();
596
- const ctx = createMockPluginContext();
597
- await expect(plugin.init(ctx as any)).resolves.toBeUndefined();
598
- });
599
- });
600
-
601
- describe('start', () => {
602
- it('should warn and skip when http server is not found', async () => {
603
- const plugin = createRestApiPlugin();
604
- const ctx = createMockPluginContext(); // no services
605
- await plugin.start!(ctx as any);
606
- expect(ctx.logger.warn).toHaveBeenCalledWith(
607
- expect.stringContaining('HTTP Server'),
608
- );
609
- });
610
-
611
- it('should warn and skip when protocol is not found', async () => {
612
- const mockServer = createMockServer();
613
- const ctx = createMockPluginContext({ 'http.server': mockServer });
614
- const plugin = createRestApiPlugin();
615
- await plugin.start!(ctx as any);
616
- expect(ctx.logger.warn).toHaveBeenCalledWith(
617
- expect.stringContaining('Protocol'),
618
- );
619
- });
620
-
621
- it('should register REST routes when both services are present', async () => {
622
- const mockServer = createMockServer();
623
- const mockProtocol = createMockProtocol();
624
- const ctx = createMockPluginContext({
625
- 'http.server': mockServer,
626
- protocol: mockProtocol,
627
- });
628
-
629
- const plugin = createRestApiPlugin();
630
- await plugin.start!(ctx as any);
631
-
632
- expect(ctx.logger.info).toHaveBeenCalledWith(
633
- expect.stringContaining('REST API successfully registered'),
634
- );
635
- // CRUD routes should have been mounted
636
- expect(mockServer.get).toHaveBeenCalled();
637
- expect(mockServer.post).toHaveBeenCalled();
638
- });
639
-
640
- it('should use custom service names from config', async () => {
641
- const mockServer = createMockServer();
642
- const mockProtocol = createMockProtocol();
643
- const ctx = createMockPluginContext({
644
- 'my.server': mockServer,
645
- 'my.protocol': mockProtocol,
646
- });
647
-
648
- const plugin = createRestApiPlugin({
649
- serverServiceName: 'my.server',
650
- protocolServiceName: 'my.protocol',
651
- });
652
- await plugin.start!(ctx as any);
653
-
654
- expect(ctx.logger.info).toHaveBeenCalledWith(
655
- expect.stringContaining('REST API successfully registered'),
656
- );
657
- });
658
-
659
- it('should throw and log error when RestServer construction fails', async () => {
660
- const badServer = {}; // missing methods → will throw
661
- const mockProtocol = createMockProtocol();
662
- const ctx = createMockPluginContext({
663
- 'http.server': badServer,
664
- protocol: mockProtocol,
665
- });
666
-
667
- const plugin = createRestApiPlugin();
668
- await expect(plugin.start!(ctx as any)).rejects.toThrow();
669
- expect(ctx.logger.error).toHaveBeenCalled();
670
- });
671
- });
672
- });