@xiboplayer/xmds 0.5.20 → 0.6.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.
@@ -1,742 +0,0 @@
1
- /**
2
- * XMDS Client Tests - REST Transport
3
- *
4
- * Tests the REST transport layer (useRestApi: true).
5
- * Verifies that REST methods produce identical data structures to SOAP.
6
- */
7
-
8
- import { describe, it, expect, beforeEach, vi } from 'vitest';
9
- import { RestClient } from './rest-client.js';
10
- import { XmdsClient } from './xmds-client.js';
11
-
12
- // ─── Helpers ──────────────────────────────────────────────────────────
13
-
14
- function createRestClient(overrides = {}) {
15
- return new RestClient({
16
- cmsUrl: 'https://cms.example.com',
17
- cmsKey: 'test-server-key',
18
- hardwareKey: 'test-hw-key',
19
- displayName: 'Test Display',
20
- xmrChannel: 'test-xmr-channel',
21
- retryOptions: { maxRetries: 0 },
22
- ...overrides,
23
- });
24
- }
25
-
26
- function jsonResponse(data, { status = 200, etag = null } = {}) {
27
- const headers = new Map([['Content-Type', 'application/json']]);
28
- if (etag) headers.set('ETag', etag);
29
- return {
30
- ok: status >= 200 && status < 400,
31
- status,
32
- statusText: status === 200 ? 'OK' : status === 304 ? 'Not Modified' : 'Error',
33
- headers: { get: (k) => headers.get(k) || null },
34
- json: async () => data,
35
- text: async () => JSON.stringify(data),
36
- };
37
- }
38
-
39
- function xmlResponse(xml, { status = 200, etag = null } = {}) {
40
- const headers = new Map([['Content-Type', 'application/xml']]);
41
- if (etag) headers.set('ETag', etag);
42
- return {
43
- ok: status >= 200 && status < 400,
44
- status,
45
- statusText: status === 200 ? 'OK' : status === 304 ? 'Not Modified' : 'Error',
46
- headers: { get: (k) => headers.get(k) || null },
47
- text: async () => xml,
48
- json: async () => { throw new Error('Not JSON'); },
49
- };
50
- }
51
-
52
- function htmlResponse(html) {
53
- const headers = new Map([['Content-Type', 'text/html']]);
54
- return {
55
- ok: true, status: 200, statusText: 'OK',
56
- headers: { get: (k) => headers.get(k) || null },
57
- text: async () => html,
58
- };
59
- }
60
-
61
- function notModifiedResponse() {
62
- return {
63
- ok: true, status: 304, statusText: 'Not Modified',
64
- headers: { get: () => null },
65
- text: async () => '',
66
- };
67
- }
68
-
69
- function errorResponse(status, message) {
70
- return {
71
- ok: false, status, statusText: message,
72
- headers: { get: () => null },
73
- text: async () => message,
74
- };
75
- }
76
-
77
- // ─── Constructor & Config ─────────────────────────────────────────────
78
-
79
- describe('RestClient - Config', () => {
80
- it('should derive REST base URL from cmsUrl', () => {
81
- const client = createRestClient();
82
- expect(client.getRestBaseUrl()).toBe('https://cms.example.com/pwa');
83
- });
84
-
85
- it('should use custom restApiUrl when provided', () => {
86
- const client = createRestClient({ restApiUrl: 'https://api.example.com/v1/pwa' });
87
- expect(client.getRestBaseUrl()).toBe('https://api.example.com/v1/pwa');
88
- });
89
-
90
- it('should strip trailing slashes from REST base URL', () => {
91
- const client = createRestClient({ restApiUrl: 'https://api.example.com/pwa/' });
92
- expect(client.getRestBaseUrl()).toBe('https://api.example.com/pwa');
93
- });
94
-
95
- it('should initialize empty ETag and response caches', () => {
96
- const client = createRestClient();
97
- expect(client._etags.size).toBe(0);
98
- expect(client._responseCache.size).toBe(0);
99
- });
100
- });
101
-
102
- // ─── REST GET with ETag Caching ───────────────────────────────────────
103
-
104
- describe('RestClient - GET & Caching', () => {
105
- let client;
106
- let mockFetch;
107
-
108
- beforeEach(() => {
109
- client = createRestClient();
110
- mockFetch = vi.fn();
111
- global.fetch = mockFetch;
112
- });
113
-
114
- it('should make GET request with serverKey and hardwareKey as query params', async () => {
115
- mockFetch.mockResolvedValue(jsonResponse({ test: true }));
116
-
117
- await client.restGet('/requiredFiles');
118
-
119
- const url = new URL(mockFetch.mock.calls[0][0]);
120
- expect(url.pathname).toContain('/pwa/requiredFiles');
121
- expect(url.searchParams.get('serverKey')).toBe('test-server-key');
122
- expect(url.searchParams.get('hardwareKey')).toBe('test-hw-key');
123
- expect(url.searchParams.get('v')).toBe('7');
124
- });
125
-
126
- it('should include additional query params', async () => {
127
- mockFetch.mockResolvedValue(htmlResponse('<div>test</div>'));
128
-
129
- await client.restGet('/resource', { layoutId: '10', regionId: '20', mediaId: '30' });
130
-
131
- const url = new URL(mockFetch.mock.calls[0][0]);
132
- expect(url.searchParams.get('layoutId')).toBe('10');
133
- expect(url.searchParams.get('regionId')).toBe('20');
134
- expect(url.searchParams.get('mediaId')).toBe('30');
135
- });
136
-
137
- it('should store ETag from response', async () => {
138
- mockFetch.mockResolvedValue(jsonResponse({ files: [] }, { etag: '"abc123"' }));
139
-
140
- await client.restGet('/requiredFiles');
141
-
142
- expect(client._etags.get('/requiredFiles')).toBe('"abc123"');
143
- });
144
-
145
- it('should send If-None-Match on subsequent requests', async () => {
146
- // First request — stores ETag
147
- mockFetch.mockResolvedValue(jsonResponse({ files: [] }, { etag: '"abc123"' }));
148
- await client.restGet('/requiredFiles');
149
-
150
- // Second request — should include If-None-Match
151
- mockFetch.mockResolvedValue(jsonResponse({ files: [] }, { etag: '"abc123"' }));
152
- await client.restGet('/requiredFiles');
153
-
154
- const secondCall = mockFetch.mock.calls[1][1];
155
- expect(secondCall.headers['If-None-Match']).toBe('"abc123"');
156
- });
157
-
158
- it('should return cached response on 304 Not Modified', async () => {
159
- const data = { file: [{ '@attributes': { type: 'media', id: '42' } }] };
160
-
161
- // First request — caches response
162
- mockFetch.mockResolvedValue(jsonResponse(data, { etag: '"etag1"' }));
163
- const first = await client.restGet('/requiredFiles');
164
-
165
- // Second request — 304
166
- mockFetch.mockResolvedValue(notModifiedResponse());
167
- const second = await client.restGet('/requiredFiles');
168
-
169
- expect(second).toEqual(first);
170
- });
171
-
172
- it('should throw on HTTP error', async () => {
173
- mockFetch.mockResolvedValue(errorResponse(500, 'Internal Server Error'));
174
-
175
- await expect(client.restGet('/requiredFiles')).rejects.toThrow('REST GET /requiredFiles failed: 500');
176
- });
177
-
178
- it('should throw on network error', async () => {
179
- mockFetch.mockRejectedValue(new Error('Connection refused'));
180
-
181
- await expect(client.restGet('/requiredFiles')).rejects.toThrow('Connection refused');
182
- });
183
- });
184
-
185
- // ─── REST POST/PUT ──────────────────────────────────────────────────
186
-
187
- describe('RestClient - POST/PUT', () => {
188
- let client;
189
- let mockFetch;
190
-
191
- beforeEach(() => {
192
- client = createRestClient();
193
- mockFetch = vi.fn();
194
- global.fetch = mockFetch;
195
- });
196
-
197
- it('should send POST with JSON body including serverKey and hardwareKey', async () => {
198
- mockFetch.mockResolvedValue(jsonResponse({ success: true }));
199
-
200
- await client.restSend('POST', '/log', { logXml: '<logs/>' });
201
-
202
- const [url, opts] = mockFetch.mock.calls[0];
203
- expect(opts.method).toBe('POST');
204
- expect(opts.headers['Content-Type']).toBe('application/json');
205
-
206
- const body = JSON.parse(opts.body);
207
- expect(body.serverKey).toBe('test-server-key');
208
- expect(body.hardwareKey).toBe('test-hw-key');
209
- expect(body.logXml).toBe('<logs/>');
210
- });
211
-
212
- it('should send PUT for status updates', async () => {
213
- mockFetch.mockResolvedValue(jsonResponse({ success: true }));
214
-
215
- await client.restSend('PUT', '/status', { statusData: { currentLayoutId: '5' } });
216
-
217
- const opts = mockFetch.mock.calls[0][1];
218
- expect(opts.method).toBe('PUT');
219
- });
220
-
221
- it('should throw on HTTP error', async () => {
222
- mockFetch.mockResolvedValue(errorResponse(403, 'Forbidden'));
223
-
224
- await expect(client.restSend('POST', '/log', {})).rejects.toThrow('REST POST /log failed: 403');
225
- });
226
- });
227
-
228
- // ─── RegisterDisplay ─────────────────────────────────────────────────
229
-
230
- describe('RestClient - RegisterDisplay', () => {
231
- let client;
232
- let mockFetch;
233
-
234
- beforeEach(() => {
235
- client = createRestClient();
236
- mockFetch = vi.fn();
237
- global.fetch = mockFetch;
238
- });
239
-
240
- it('should include xmrPubKey from config in POST body', async () => {
241
- const clientWithKey = createRestClient({
242
- xmrPubKey: '-----BEGIN PUBLIC KEY-----\nTEST\n-----END PUBLIC KEY-----',
243
- });
244
- const mockFetchLocal = vi.fn();
245
- global.fetch = mockFetchLocal;
246
-
247
- mockFetchLocal.mockResolvedValue(jsonResponse({
248
- display: {
249
- '@attributes': { code: 'READY', message: 'OK' },
250
- collectInterval: '60',
251
- }
252
- }));
253
-
254
- await clientWithKey.registerDisplay();
255
-
256
- const body = JSON.parse(mockFetchLocal.mock.calls[0][1].body);
257
- expect(body.xmrPubKey).toBe('-----BEGIN PUBLIC KEY-----\nTEST\n-----END PUBLIC KEY-----');
258
- });
259
-
260
- it('should send empty xmrPubKey when config has no key', async () => {
261
- mockFetch.mockResolvedValue(jsonResponse({
262
- display: {
263
- '@attributes': { code: 'READY', message: 'OK' },
264
- collectInterval: '60',
265
- }
266
- }));
267
-
268
- await client.registerDisplay();
269
-
270
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
271
- expect(body.xmrPubKey).toBe('');
272
- });
273
-
274
- it('should POST to /register and parse READY response', async () => {
275
- mockFetch.mockResolvedValue(jsonResponse({
276
- display: {
277
- '@attributes': { code: 'READY', message: 'Display is active', checkRf: 'rf123', checkSchedule: 'sc456' },
278
- collectInterval: '60',
279
- downloadStartWindow: '00:00',
280
- downloadEndWindow: '00:00',
281
- statsEnabled: '1',
282
- screenShotRequestInterval: '300',
283
- }
284
- }));
285
-
286
- const result = await client.registerDisplay();
287
-
288
- expect(result.code).toBe('READY');
289
- expect(result.message).toBe('Display is active');
290
- expect(result.settings.collectInterval).toBe('60');
291
- expect(result.settings.statsEnabled).toBe('1');
292
- expect(result.checkRf).toBe('rf123');
293
- expect(result.checkSchedule).toBe('sc456');
294
-
295
- // Verify POST method and body
296
- const opts = mockFetch.mock.calls[0][1];
297
- expect(opts.method).toBe('POST');
298
- const body = JSON.parse(opts.body);
299
- expect(body.displayName).toBe('Test Display');
300
- expect(body.clientType).toBe('chromeOS');
301
- });
302
-
303
- it('should handle WAITING response', async () => {
304
- mockFetch.mockResolvedValue(jsonResponse({
305
- display: {
306
- '@attributes': { code: 'WAITING', message: 'Display is awaiting authorisation' },
307
- }
308
- }));
309
-
310
- const result = await client.registerDisplay();
311
-
312
- expect(result.code).toBe('WAITING');
313
- expect(result.settings).toBeNull();
314
- });
315
-
316
- it('should handle flat JSON (no @attributes wrapper)', async () => {
317
- mockFetch.mockResolvedValue(jsonResponse({
318
- code: 'READY',
319
- message: 'OK',
320
- collectInterval: '30',
321
- }));
322
-
323
- const result = await client.registerDisplay();
324
- expect(result.code).toBe('READY');
325
- expect(result.settings.collectInterval).toBe('30');
326
- });
327
-
328
- it('should throw on HTTP error', async () => {
329
- mockFetch.mockResolvedValue(errorResponse(500, 'Server Error'));
330
-
331
- await expect(client.registerDisplay()).rejects.toThrow('REST POST /register failed: 500');
332
- });
333
- });
334
-
335
- // ─── RequiredFiles ───────────────────────────────────────────────────
336
-
337
- describe('RestClient - RequiredFiles', () => {
338
- let client;
339
- let mockFetch;
340
-
341
- beforeEach(() => {
342
- client = createRestClient();
343
- mockFetch = vi.fn();
344
- global.fetch = mockFetch;
345
- });
346
-
347
- it('should GET /requiredFiles and parse file manifest', async () => {
348
- mockFetch.mockResolvedValue(jsonResponse({
349
- file: [
350
- { '@attributes': { type: 'media', id: '42', size: '1024', md5: 'abc', download: 'http', path: '/media/42.jpg' } },
351
- { '@attributes': { type: 'layout', id: '10', size: '500', md5: 'def', download: 'xmds', path: null } },
352
- { '@attributes': { type: 'resource', id: '99', size: '200', md5: 'ghi', download: 'xmds', layoutid: '10', regionid: '5', mediaid: '99' } },
353
- ]
354
- }, { etag: '"files-v1"' }));
355
-
356
- const result = await client.requiredFiles();
357
-
358
- expect(result.files).toHaveLength(3);
359
- expect(result.files[0]).toEqual(expect.objectContaining({
360
- type: 'media', id: '42', size: 1024, md5: 'abc', download: 'http',
361
- path: '/media/42.jpg', code: null, layoutid: null, regionid: null, mediaid: null,
362
- }));
363
- expect(result.files[1].type).toBe('layout');
364
- expect(result.files[2].layoutid).toBe('10');
365
- expect(result.files[2].regionid).toBe('5');
366
- expect(result.files[2].mediaid).toBe('99');
367
- expect(result.purge).toEqual([]);
368
- });
369
-
370
- it('should handle single file (not array)', async () => {
371
- mockFetch.mockResolvedValue(jsonResponse({
372
- file: { '@attributes': { type: 'media', id: '1', size: '100', md5: 'x' } }
373
- }));
374
-
375
- const result = await client.requiredFiles();
376
- expect(result.files).toHaveLength(1);
377
- expect(result.files[0].id).toBe('1');
378
- });
379
-
380
- it('should handle empty file list', async () => {
381
- mockFetch.mockResolvedValue(jsonResponse({}));
382
-
383
- const result = await client.requiredFiles();
384
- expect(result.files).toHaveLength(0);
385
- });
386
-
387
- it('should use ETag caching', async () => {
388
- const data = { file: [{ '@attributes': { type: 'media', id: '1', size: '100', md5: 'x' } }] };
389
-
390
- // First call — stores ETag
391
- mockFetch.mockResolvedValue(jsonResponse(data, { etag: '"rf-v1"' }));
392
- const first = await client.requiredFiles();
393
-
394
- // Second call — 304
395
- mockFetch.mockResolvedValue(notModifiedResponse());
396
- const second = await client.requiredFiles();
397
-
398
- // Should get same cached JSON (not yet parsed)
399
- expect(second).toEqual(first);
400
- });
401
-
402
- it('should parse required files JSON into standard structure', () => {
403
- const json = {
404
- file: [
405
- { '@attributes': { type: 'media', id: '42', size: '1024', md5: 'abc', download: 'http', path: '/media/42.jpg' } },
406
- ]
407
- };
408
-
409
- const result = client._parseRequiredFilesJson(json);
410
-
411
- expect(result.files[0].type).toBe('media');
412
- expect(result.files[0].id).toBe('42');
413
- expect(result.files[0].size).toBe(1024);
414
- expect(result.files[0].md5).toBe('abc');
415
- expect(result.files[0].download).toBe('http');
416
- expect(result.files[0].path).toBe('/media/42.jpg');
417
- });
418
- });
419
-
420
- // ─── Schedule ────────────────────────────────────────────────────────
421
-
422
- describe('RestClient - Schedule', () => {
423
- let client;
424
- let mockFetch;
425
-
426
- beforeEach(() => {
427
- client = createRestClient();
428
- mockFetch = vi.fn();
429
- global.fetch = mockFetch;
430
- });
431
-
432
- it('should GET /schedule and parse XML response', async () => {
433
- const scheduleXml = `<?xml version="1.0" encoding="UTF-8"?>
434
- <schedule>
435
- <default file="100" />
436
- <layout file="200" fromdt="2026-01-01" todt="2026-12-31" scheduleid="1" priority="5" />
437
- <campaign id="c1" priority="10" fromdt="2026-01-01" todt="2026-12-31" scheduleid="2">
438
- <layout file="300" />
439
- <layout file="301" />
440
- </campaign>
441
- </schedule>`;
442
-
443
- mockFetch.mockResolvedValue(xmlResponse(scheduleXml, { etag: '"sched-v1"' }));
444
-
445
- const schedule = await client.schedule();
446
-
447
- expect(schedule.default).toBe('100');
448
- expect(schedule.layouts).toHaveLength(1);
449
- expect(schedule.layouts[0].file).toBe('200');
450
- expect(schedule.layouts[0].priority).toBe(5);
451
- expect(schedule.campaigns).toHaveLength(1);
452
- expect(schedule.campaigns[0].layouts).toHaveLength(2);
453
- expect(schedule.campaigns[0].layouts[0].file).toBe('300');
454
- });
455
-
456
- it('should use ETag caching for schedule', async () => {
457
- const xml = '<schedule><default file="1" /></schedule>';
458
-
459
- mockFetch.mockResolvedValue(xmlResponse(xml, { etag: '"s1"' }));
460
- const first = await client.schedule();
461
-
462
- // 304 — return cached
463
- mockFetch.mockResolvedValue(notModifiedResponse());
464
- const second = await client.schedule();
465
-
466
- expect(second.default).toBe(first.default);
467
- });
468
-
469
- it('should parse overlays', async () => {
470
- const xml = `<schedule>
471
- <default file="1"/>
472
- <overlays>
473
- <overlay file="50" duration="30" fromdt="2026-01-01" todt="2026-12-31" priority="10" scheduleid="5" />
474
- </overlays>
475
- </schedule>`;
476
-
477
- mockFetch.mockResolvedValue(xmlResponse(xml));
478
-
479
- const schedule = await client.schedule();
480
- expect(schedule.overlays).toHaveLength(1);
481
- expect(schedule.overlays[0].file).toBe('50');
482
- expect(schedule.overlays[0].duration).toBe(30);
483
- expect(schedule.overlays[0].priority).toBe(10);
484
- });
485
- });
486
-
487
- // ─── GetResource ─────────────────────────────────────────────────────
488
-
489
- describe('RestClient - GetResource', () => {
490
- let client;
491
- let mockFetch;
492
-
493
- beforeEach(() => {
494
- client = createRestClient();
495
- mockFetch = vi.fn();
496
- global.fetch = mockFetch;
497
- });
498
-
499
- it('should GET /resource with layout/region/media params', async () => {
500
- mockFetch.mockResolvedValue(htmlResponse('<html><body>Widget HTML</body></html>'));
501
-
502
- const html = await client.getResource(10, 20, 30);
503
-
504
- expect(html).toContain('Widget HTML');
505
-
506
- const url = new URL(mockFetch.mock.calls[0][0]);
507
- expect(url.searchParams.get('layoutId')).toBe('10');
508
- expect(url.searchParams.get('regionId')).toBe('20');
509
- expect(url.searchParams.get('mediaId')).toBe('30');
510
- });
511
-
512
- it('should return raw HTML string', async () => {
513
- const widgetHtml = '<div style="color:red">Hello World</div>';
514
- mockFetch.mockResolvedValue(htmlResponse(widgetHtml));
515
-
516
- const result = await client.getResource(1, 2, 3);
517
- expect(result).toBe(widgetHtml);
518
- });
519
- });
520
-
521
- // ─── NotifyStatus ────────────────────────────────────────────────────
522
-
523
- describe('RestClient - NotifyStatus', () => {
524
- let client;
525
- let mockFetch;
526
-
527
- beforeEach(() => {
528
- client = createRestClient();
529
- mockFetch = vi.fn();
530
- global.fetch = mockFetch;
531
- });
532
-
533
- it('should PUT to /status with JSON statusData', async () => {
534
- mockFetch.mockResolvedValue(jsonResponse({ success: true }));
535
-
536
- const status = { currentLayoutId: '5', timeZone: 'Europe/Madrid' };
537
- await client.notifyStatus(status);
538
-
539
- const [url, opts] = mockFetch.mock.calls[0];
540
- expect(opts.method).toBe('PUT');
541
- expect(new URL(url).pathname).toContain('/pwa/status');
542
-
543
- const body = JSON.parse(opts.body);
544
- expect(body.statusData.currentLayoutId).toBe('5');
545
- });
546
-
547
- it('should return success response', async () => {
548
- mockFetch.mockResolvedValue(jsonResponse({ success: true }));
549
-
550
- const result = await client.notifyStatus({ currentLayoutId: '1', timeZone: 'UTC' });
551
- expect(result.success).toBe(true);
552
- });
553
- });
554
-
555
- // ─── SubmitLog ───────────────────────────────────────────────────────
556
-
557
- describe('RestClient - SubmitLog', () => {
558
- let client;
559
- let mockFetch;
560
-
561
- beforeEach(() => {
562
- client = createRestClient();
563
- mockFetch = vi.fn();
564
- global.fetch = mockFetch;
565
- });
566
-
567
- it('should POST to /log with logXml', async () => {
568
- mockFetch.mockResolvedValue(jsonResponse({ success: true }));
569
-
570
- const result = await client.submitLog('<logs><log date="2026-01-01" /></logs>');
571
-
572
- expect(result).toBe(true);
573
-
574
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
575
- expect(body.logXml).toContain('<logs>');
576
- });
577
-
578
- it('should return false when server responds with success: false', async () => {
579
- mockFetch.mockResolvedValue(jsonResponse({ success: false }));
580
-
581
- const result = await client.submitLog('<logs/>');
582
- expect(result).toBe(false);
583
- });
584
-
585
- it('should throw on HTTP error', async () => {
586
- mockFetch.mockResolvedValue(errorResponse(500, 'Server Error'));
587
-
588
- await expect(client.submitLog('<logs/>')).rejects.toThrow('REST POST /log failed: 500');
589
- });
590
- });
591
-
592
- // ─── SubmitStats ─────────────────────────────────────────────────────
593
-
594
- describe('RestClient - SubmitStats', () => {
595
- let client;
596
- let mockFetch;
597
-
598
- beforeEach(() => {
599
- client = createRestClient();
600
- mockFetch = vi.fn();
601
- global.fetch = mockFetch;
602
- });
603
-
604
- it('should POST to /stats with statXml', async () => {
605
- mockFetch.mockResolvedValue(jsonResponse({ success: true }));
606
-
607
- const result = await client.submitStats('<stats><stat type="layout" /></stats>');
608
-
609
- expect(result).toBe(true);
610
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
611
- expect(body.statXml).toContain('<stats>');
612
- });
613
-
614
- it('should return false on failure', async () => {
615
- mockFetch.mockResolvedValue(jsonResponse({ success: false }));
616
-
617
- const result = await client.submitStats('<stats/>');
618
- expect(result).toBe(false);
619
- });
620
-
621
- it('should throw on HTTP error', async () => {
622
- mockFetch.mockResolvedValue(errorResponse(500, 'Error'));
623
-
624
- await expect(client.submitStats('<stats/>')).rejects.toThrow('REST POST /stats failed: 500');
625
- });
626
- });
627
-
628
- // ─── SubmitScreenShot ────────────────────────────────────────────────
629
-
630
- describe('RestClient - SubmitScreenShot', () => {
631
- let client;
632
- let mockFetch;
633
-
634
- beforeEach(() => {
635
- client = createRestClient();
636
- mockFetch = vi.fn();
637
- global.fetch = mockFetch;
638
- });
639
-
640
- it('should POST to /screenshot with base64 image', async () => {
641
- mockFetch.mockResolvedValue(jsonResponse({ success: true }));
642
-
643
- const result = await client.submitScreenShot('iVBORw0KGgo...');
644
-
645
- expect(result).toBe(true);
646
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
647
- expect(body.screenshot).toBe('iVBORw0KGgo...');
648
- });
649
-
650
- it('should return false on failure', async () => {
651
- mockFetch.mockResolvedValue(jsonResponse({ success: false }));
652
-
653
- const result = await client.submitScreenShot('data');
654
- expect(result).toBe(false);
655
- });
656
- });
657
-
658
- // ─── MediaInventory ──────────────────────────────────────────────────
659
-
660
- describe('RestClient - MediaInventory', () => {
661
- let client;
662
- let mockFetch;
663
-
664
- beforeEach(() => {
665
- client = createRestClient();
666
- mockFetch = vi.fn();
667
- global.fetch = mockFetch;
668
- });
669
-
670
- it('should POST to /mediaInventory with inventory XML', async () => {
671
- mockFetch.mockResolvedValue(jsonResponse({ success: true }));
672
-
673
- await client.mediaInventory('<files><file type="media" id="1" complete="1"/></files>');
674
-
675
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
676
- expect(body.inventory).toContain('<files>');
677
- });
678
- });
679
-
680
- // ─── BlackList ───────────────────────────────────────────────────────
681
-
682
- describe('RestClient - BlackList', () => {
683
- let mockFetch;
684
-
685
- beforeEach(() => {
686
- mockFetch = vi.fn();
687
- global.fetch = mockFetch;
688
- });
689
-
690
- it('should POST to /blacklist endpoint', async () => {
691
- mockFetch.mockResolvedValueOnce({
692
- ok: true,
693
- status: 200,
694
- headers: { get: () => 'application/json' },
695
- json: () => Promise.resolve({ success: true }),
696
- });
697
-
698
- const client = createRestClient();
699
- const result = await client.blackList('42', 'media', 'Broken file');
700
-
701
- expect(result).toBe(true);
702
- expect(mockFetch).toHaveBeenCalledWith(
703
- expect.stringContaining('/blacklist'),
704
- expect.objectContaining({ method: 'POST' })
705
- );
706
- const body = JSON.parse(mockFetch.mock.calls[0][1].body);
707
- expect(body.mediaId).toBe('42');
708
- expect(body.type).toBe('media');
709
- expect(body.reason).toBe('Broken file');
710
- });
711
-
712
- it('should return false on failure', async () => {
713
- mockFetch.mockRejectedValue(new Error('Network error'));
714
-
715
- const client = createRestClient();
716
- const result = await client.blackList('42', 'media', 'Broken');
717
- expect(result).toBe(false);
718
- });
719
- });
720
-
721
- // ─── Transport Parity ────────────────────────────────────────────────
722
-
723
- describe('Transport Parity', () => {
724
- it('SOAP and REST clients should expose identical public methods', () => {
725
- const soap = new XmdsClient({
726
- cmsUrl: 'https://cms.example.com',
727
- cmsKey: 'k', hardwareKey: 'h',
728
- });
729
- const rest = createRestClient();
730
-
731
- const publicMethods = [
732
- 'registerDisplay', 'requiredFiles', 'schedule', 'getResource',
733
- 'notifyStatus', 'mediaInventory', 'blackList', 'submitLog',
734
- 'submitScreenShot', 'submitStats',
735
- ];
736
-
737
- for (const method of publicMethods) {
738
- expect(typeof soap[method]).toBe('function');
739
- expect(typeof rest[method]).toBe('function');
740
- }
741
- });
742
- });