@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.
- package/package.json +2 -2
- package/src/index.d.ts +11 -7
- package/src/rest-client.js +242 -166
- package/src/xmds-client.js +1 -1
- package/src/xmds.rest.integration.test.js +172 -508
- package/src/xmds.test.js +2 -2
- package/src/xmds.rest.test.js +0 -742
|
@@ -1,28 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* REST API — Live Integration Tests
|
|
3
3
|
*
|
|
4
|
-
* Tests the
|
|
5
|
-
*
|
|
6
|
-
* (REST mode) and the CMS Player REST API endpoints.
|
|
4
|
+
* Tests the RestClient transport against a real Xibo CMS instance
|
|
5
|
+
* with the Player API module deployed.
|
|
7
6
|
*
|
|
8
7
|
* Prerequisites:
|
|
9
|
-
* - CMS at CMS_URL must have
|
|
8
|
+
* - CMS at CMS_URL must have /api/v2/player/* endpoints deployed
|
|
10
9
|
* - A display with the given HARDWARE_KEY must exist and be authorized
|
|
11
10
|
* - The SERVER_KEY must match the CMS setting
|
|
12
11
|
*
|
|
13
12
|
* Run with:
|
|
14
|
-
* CMS_URL=https://
|
|
15
|
-
* CMS_KEY=
|
|
16
|
-
* HARDWARE_KEY=pwa-
|
|
13
|
+
* CMS_URL=https://displays.superpantalles.com \
|
|
14
|
+
* CMS_KEY=isiSdUCy \
|
|
15
|
+
* HARDWARE_KEY=pwa-11e79847294d418ba74df4ba534d \
|
|
17
16
|
* npx vitest run src/xmds.rest.integration.test.js
|
|
18
|
-
*
|
|
19
|
-
* Or:
|
|
20
|
-
* npm test -- --testPathPattern=integration
|
|
21
17
|
*/
|
|
22
18
|
|
|
23
|
-
import { describe, it, expect, beforeAll
|
|
19
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
24
20
|
import { RestClient } from './rest-client.js';
|
|
25
|
-
import { XmdsClient } from './xmds-client.js';
|
|
26
21
|
|
|
27
22
|
// ─── Configuration ─────────────────────────────────────────────────
|
|
28
23
|
|
|
@@ -31,219 +26,170 @@ const CMS_KEY = process.env.CMS_KEY || 'your-cms-key';
|
|
|
31
26
|
const HARDWARE_KEY = process.env.HARDWARE_KEY || 'pwa-your-hardware-key';
|
|
32
27
|
const DISPLAY_NAME = process.env.DISPLAY_NAME || 'REST Integration Test';
|
|
33
28
|
|
|
34
|
-
// Skip all tests if no CMS_URL is provided
|
|
29
|
+
// Skip all tests if no CMS_URL is provided
|
|
35
30
|
const SKIP = !process.env.CMS_URL && !process.env.CI && !process.env.RUN_INTEGRATION;
|
|
36
31
|
|
|
37
32
|
// ─── Test Suite ────────────────────────────────────────────────────
|
|
38
33
|
|
|
39
|
-
describe.skipIf(SKIP)('
|
|
34
|
+
describe.skipIf(SKIP)('REST API — Live Integration', () => {
|
|
40
35
|
/** @type {RestClient} */
|
|
41
36
|
let client;
|
|
42
|
-
/** @type {XmdsClient} */
|
|
43
|
-
let soapClient;
|
|
44
37
|
|
|
45
38
|
beforeAll(() => {
|
|
46
39
|
// Restore real fetch (vitest.setup.js mocks it for unit tests)
|
|
47
|
-
global.fetch = global.__nativeFetch;
|
|
40
|
+
if (global.__nativeFetch) global.fetch = global.__nativeFetch;
|
|
48
41
|
|
|
49
|
-
// REST client
|
|
50
42
|
client = new RestClient({
|
|
51
43
|
cmsUrl: CMS_URL,
|
|
52
44
|
cmsKey: CMS_KEY,
|
|
53
45
|
hardwareKey: HARDWARE_KEY,
|
|
54
46
|
displayName: DISPLAY_NAME,
|
|
55
|
-
xmrChannel: 'integration-test
|
|
56
|
-
retryOptions: { maxRetries: 1, baseDelayMs: 500 },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// SOAP client for parity comparison
|
|
60
|
-
soapClient = new XmdsClient({
|
|
61
|
-
cmsUrl: CMS_URL,
|
|
62
|
-
cmsKey: CMS_KEY,
|
|
63
|
-
hardwareKey: HARDWARE_KEY,
|
|
64
|
-
displayName: DISPLAY_NAME,
|
|
65
|
-
xmrChannel: 'integration-test-channel',
|
|
47
|
+
xmrChannel: 'rest-integration-test',
|
|
66
48
|
retryOptions: { maxRetries: 1, baseDelayMs: 500 },
|
|
67
49
|
});
|
|
68
50
|
});
|
|
69
51
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
describe('RegisterDisplay', () => {
|
|
75
|
-
it('should register and return READY for an authorized display', async () => {
|
|
76
|
-
const result = await client.registerDisplay();
|
|
52
|
+
// ──────────────────────────────────────────────────────────────────
|
|
53
|
+
// Health & Availability
|
|
54
|
+
// ──────────────────────────────────────────────────────────────────
|
|
77
55
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (result.code === 'READY') {
|
|
83
|
-
expect(result.settings).toBeDefined();
|
|
84
|
-
expect(result.settings).not.toBeNull();
|
|
85
|
-
expect(result.message).toContain('Display is active');
|
|
86
|
-
} else {
|
|
87
|
-
expect(result.message).toContain('awaiting');
|
|
88
|
-
}
|
|
56
|
+
describe('Health & Availability', () => {
|
|
57
|
+
it('should detect REST availability via static isAvailable()', async () => {
|
|
58
|
+
const available = await RestClient.isAvailable(CMS_URL);
|
|
59
|
+
expect(available).toBe(true);
|
|
89
60
|
});
|
|
90
61
|
|
|
91
|
-
it('should return
|
|
92
|
-
const
|
|
62
|
+
it('should return false for a CMS without REST API', async () => {
|
|
63
|
+
const available = await RestClient.isAvailable('http://127.0.0.1:1', { maxRetries: 0 });
|
|
64
|
+
expect(available).toBe(false);
|
|
65
|
+
}, 10000);
|
|
66
|
+
});
|
|
93
67
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
68
|
+
// ──────────────────────────────────────────────────────────────────
|
|
69
|
+
// JWT Authentication
|
|
70
|
+
// ──────────────────────────────────────────────────────────────────
|
|
98
71
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
'statsEnabled',
|
|
104
|
-
'xmrNetworkAddress',
|
|
105
|
-
];
|
|
72
|
+
describe('JWT Authentication', () => {
|
|
73
|
+
it('should authenticate and obtain a JWT token', async () => {
|
|
74
|
+
client._token = null;
|
|
75
|
+
await client._authenticate();
|
|
106
76
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
77
|
+
expect(client._token).toBeDefined();
|
|
78
|
+
expect(client._token.length).toBeGreaterThan(50);
|
|
79
|
+
expect(client._displayId).toBeGreaterThan(0);
|
|
80
|
+
expect(client._tokenExpiresAt).toBeGreaterThan(Date.now());
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should reuse token for subsequent calls', async () => {
|
|
84
|
+
const token1 = await client._getToken();
|
|
85
|
+
const token2 = await client._getToken();
|
|
86
|
+
expect(token1).toBe(token2);
|
|
110
87
|
});
|
|
111
88
|
|
|
112
|
-
it('should fail
|
|
113
|
-
const
|
|
89
|
+
it('should fail auth with wrong server key', async () => {
|
|
90
|
+
const bad = new RestClient({
|
|
114
91
|
cmsUrl: CMS_URL,
|
|
115
92
|
cmsKey: 'wrong-key',
|
|
116
93
|
hardwareKey: HARDWARE_KEY,
|
|
117
|
-
displayName: DISPLAY_NAME,
|
|
118
|
-
xmrChannel: 'test',
|
|
119
94
|
retryOptions: { maxRetries: 0 },
|
|
120
95
|
});
|
|
121
96
|
|
|
122
|
-
|
|
123
|
-
// So we expect either an Error or a result with error info
|
|
124
|
-
try {
|
|
125
|
-
const result = await badClient.registerDisplay();
|
|
126
|
-
// If it doesn't throw, the code should indicate failure
|
|
127
|
-
expect(result.code).not.toBe('READY');
|
|
128
|
-
} catch (e) {
|
|
129
|
-
expect(e.message).toMatch(/server key|failed|400|500/i);
|
|
130
|
-
}
|
|
97
|
+
await expect(bad._authenticate()).rejects.toThrow(/403|forbidden|server key/i);
|
|
131
98
|
});
|
|
132
99
|
|
|
133
|
-
it('should
|
|
134
|
-
const
|
|
135
|
-
|
|
100
|
+
it('should fail auth with wrong hardware key', async () => {
|
|
101
|
+
const bad = new RestClient({
|
|
102
|
+
cmsUrl: CMS_URL,
|
|
103
|
+
cmsKey: CMS_KEY,
|
|
104
|
+
hardwareKey: 'nonexistent-display',
|
|
105
|
+
retryOptions: { maxRetries: 0 },
|
|
106
|
+
});
|
|
136
107
|
|
|
137
|
-
expect(
|
|
108
|
+
await expect(bad._authenticate()).rejects.toThrow(/403|not found|denied/i);
|
|
138
109
|
});
|
|
139
110
|
});
|
|
140
111
|
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
112
|
+
// ──────────────────────────────────────────────────────────────────
|
|
113
|
+
// RegisterDisplay
|
|
114
|
+
// ──────────────────────────────────────────────────────────────────
|
|
144
115
|
|
|
145
|
-
describe('
|
|
146
|
-
it('should
|
|
147
|
-
const
|
|
116
|
+
describe('RegisterDisplay', () => {
|
|
117
|
+
it('should register and return READY', async () => {
|
|
118
|
+
const result = await client.registerDisplay();
|
|
148
119
|
|
|
149
|
-
expect(
|
|
150
|
-
expect(
|
|
120
|
+
expect(result).toBeDefined();
|
|
121
|
+
expect(result.code).toBe('READY');
|
|
122
|
+
expect(result.settings).toBeDefined();
|
|
123
|
+
expect(result.message).toContain('Display is active');
|
|
151
124
|
});
|
|
152
125
|
|
|
153
|
-
it('should include
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (mediaFiles.length === 0) {
|
|
158
|
-
console.warn('No media files in manifest — skipping');
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
126
|
+
it('should include expected settings keys', async () => {
|
|
127
|
+
const result = await client.registerDisplay();
|
|
128
|
+
if (result.code !== 'READY') return;
|
|
161
129
|
|
|
162
|
-
for (const
|
|
163
|
-
expect(
|
|
164
|
-
expect(file.size).toBeGreaterThan(0);
|
|
165
|
-
expect(file.md5).toBeDefined();
|
|
166
|
-
expect(file.md5).toHaveLength(32);
|
|
167
|
-
expect(file.path).toBeDefined();
|
|
130
|
+
for (const key of ['collectInterval', 'statsEnabled', 'xmrNetworkAddress']) {
|
|
131
|
+
expect(result.settings, `Missing setting: ${key}`).toHaveProperty(key);
|
|
168
132
|
}
|
|
169
133
|
});
|
|
134
|
+
});
|
|
170
135
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
136
|
+
// ──────────────────────────────────────────────────────────────────
|
|
137
|
+
// RequiredFiles (media)
|
|
138
|
+
// ──────────────────────────────────────────────────────────────────
|
|
174
139
|
|
|
175
|
-
|
|
140
|
+
describe('RequiredFiles', () => {
|
|
141
|
+
it('should return files in flat format', async () => {
|
|
142
|
+
const result = await client.requiredFiles();
|
|
176
143
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
144
|
+
expect(result).toHaveProperty('files');
|
|
145
|
+
expect(result).toHaveProperty('purge');
|
|
146
|
+
expect(Array.isArray(result.files)).toBe(true);
|
|
147
|
+
expect(result.files.length).toBeGreaterThan(0);
|
|
182
148
|
});
|
|
183
149
|
|
|
184
|
-
it('should include
|
|
185
|
-
const files = await client.requiredFiles();
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
if (resourceFiles.length === 0) {
|
|
189
|
-
console.warn('No resource files — skipping');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
150
|
+
it('should include media files with download URLs', async () => {
|
|
151
|
+
const { files } = await client.requiredFiles();
|
|
152
|
+
const media = files.filter(f => f.type === 'media');
|
|
192
153
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
expect(file.
|
|
196
|
-
expect(file.
|
|
154
|
+
expect(media.length).toBeGreaterThan(0);
|
|
155
|
+
for (const file of media) {
|
|
156
|
+
expect(file.id).toBeDefined();
|
|
157
|
+
expect(file.md5).toBeDefined();
|
|
158
|
+
expect(file.path).toMatch(/^https?:\/\//);
|
|
197
159
|
}
|
|
198
160
|
});
|
|
199
161
|
|
|
200
|
-
it('should
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
expect(files1.length).toBeGreaterThan(0);
|
|
204
|
-
|
|
205
|
-
// Second call should get 304 + cached result
|
|
206
|
-
const files2 = await client.requiredFiles();
|
|
207
|
-
expect(files2.length).toBe(files1.length);
|
|
162
|
+
it('should include layout files', async () => {
|
|
163
|
+
const { files } = await client.requiredFiles();
|
|
164
|
+
const layouts = files.filter(f => f.type === 'layout');
|
|
208
165
|
|
|
209
|
-
|
|
210
|
-
|
|
166
|
+
expect(layouts.length).toBeGreaterThan(0);
|
|
167
|
+
for (const layout of layouts) {
|
|
168
|
+
expect(layout.id).toBeDefined();
|
|
169
|
+
expect(layout.md5).toBeDefined();
|
|
170
|
+
}
|
|
211
171
|
});
|
|
212
172
|
|
|
213
|
-
it('should
|
|
214
|
-
// Clear cache for fair comparison
|
|
173
|
+
it('should support ETag caching', async () => {
|
|
215
174
|
client._etags.clear();
|
|
216
175
|
client._responseCache.clear();
|
|
217
176
|
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
expect(restFiles.length).toBe(soapFiles.length);
|
|
222
|
-
|
|
223
|
-
// Compare file IDs (order may differ)
|
|
224
|
-
const restIds = restFiles.map(f => `${f.type}-${f.id}`).sort();
|
|
225
|
-
const soapIds = soapFiles.map(f => `${f.type}-${f.id}`).sort();
|
|
226
|
-
expect(restIds).toEqual(soapIds);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should include download URLs for media files', async () => {
|
|
230
|
-
const files = await client.requiredFiles();
|
|
231
|
-
const mediaFiles = files.filter(f => f.type === 'media');
|
|
177
|
+
await client.requiredFiles();
|
|
178
|
+
const hasEtag = client._etags.has(`/displays/${client._displayId}/media`);
|
|
179
|
+
expect(hasEtag).toBe(true);
|
|
232
180
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
expect(file.path).toMatch(/^https?:\/\//);
|
|
237
|
-
}
|
|
181
|
+
// Second call should use cache
|
|
182
|
+
const result2 = await client.requiredFiles();
|
|
183
|
+
expect(result2.files.length).toBeGreaterThan(0);
|
|
238
184
|
});
|
|
239
185
|
});
|
|
240
186
|
|
|
241
|
-
//
|
|
187
|
+
// ──────────────────────────────────────────────────────────────────
|
|
242
188
|
// Schedule
|
|
243
|
-
//
|
|
189
|
+
// ──────────────────────────────────────────────────────────────────
|
|
244
190
|
|
|
245
191
|
describe('Schedule', () => {
|
|
246
|
-
it('should return
|
|
192
|
+
it('should return native JSON schedule', async () => {
|
|
247
193
|
const schedule = await client.schedule();
|
|
248
194
|
|
|
249
195
|
expect(schedule).toBeDefined();
|
|
@@ -252,408 +198,126 @@ describe.skipIf(SKIP)('XMDS REST API — Live Integration', () => {
|
|
|
252
198
|
expect(Array.isArray(schedule.layouts)).toBe(true);
|
|
253
199
|
});
|
|
254
200
|
|
|
255
|
-
it('should include layout
|
|
201
|
+
it('should include default layout', async () => {
|
|
256
202
|
const schedule = await client.schedule();
|
|
257
|
-
|
|
258
|
-
if (schedule.layouts.length === 0 && !schedule.default) {
|
|
259
|
-
console.warn('Empty schedule — skipping');
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
for (const layout of schedule.layouts) {
|
|
264
|
-
expect(layout.file).toBeDefined();
|
|
265
|
-
expect(layout.fromdt).toBeDefined();
|
|
266
|
-
expect(layout.todt).toBeDefined();
|
|
267
|
-
expect(layout.scheduleid).toBeDefined();
|
|
268
|
-
expect(layout.priority).toBeDefined();
|
|
269
|
-
}
|
|
203
|
+
expect(schedule.default).toBeDefined();
|
|
270
204
|
});
|
|
271
205
|
|
|
272
206
|
it('should support ETag caching', async () => {
|
|
273
207
|
client._etags.clear();
|
|
274
208
|
client._responseCache.clear();
|
|
275
209
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
// Both should return same data
|
|
280
|
-
expect(schedule1.layouts.length).toBe(schedule2.layouts.length);
|
|
281
|
-
|
|
282
|
-
// ETag should be cached
|
|
283
|
-
expect(client._etags.has('/schedule')).toBe(true);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('should match SOAP schedule layout count', async () => {
|
|
287
|
-
client._etags.clear();
|
|
288
|
-
client._responseCache.clear();
|
|
289
|
-
|
|
290
|
-
const restSchedule = await client.schedule();
|
|
291
|
-
const soapSchedule = await soapClient.schedule();
|
|
292
|
-
|
|
293
|
-
expect(restSchedule.layouts.length).toBe(soapSchedule.layouts.length);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it('should include schedule metadata (default layout, dependents)', async () => {
|
|
297
|
-
const schedule = await client.schedule();
|
|
298
|
-
|
|
299
|
-
// Check layouts have dependents array
|
|
300
|
-
for (const layout of schedule.layouts) {
|
|
301
|
-
if (layout.dependents) {
|
|
302
|
-
expect(Array.isArray(layout.dependents)).toBe(true);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
210
|
+
await client.schedule();
|
|
211
|
+
expect(client._etags.has(`/displays/${client._displayId}/schedule`)).toBe(true);
|
|
305
212
|
});
|
|
306
213
|
});
|
|
307
214
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
describe('GetResource', () => {
|
|
313
|
-
let resourceFiles;
|
|
215
|
+
// ──────────────────────────────────────────────────────────────────
|
|
216
|
+
// Reporting Endpoints
|
|
217
|
+
// ──────────────────────────────────────────────────────────────────
|
|
314
218
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
it('should fetch a widget resource as HTML', async () => {
|
|
321
|
-
if (resourceFiles.length === 0) {
|
|
322
|
-
console.warn('No resources to test — skipping');
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const res = resourceFiles[0];
|
|
327
|
-
const html = await client.getResource(res.layoutid, res.regionid, res.mediaid);
|
|
328
|
-
|
|
329
|
-
expect(html).toBeDefined();
|
|
330
|
-
expect(typeof html).toBe('string');
|
|
331
|
-
expect(html.length).toBeGreaterThan(0);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('should return valid HTML for all resource files', async () => {
|
|
335
|
-
if (resourceFiles.length === 0) {
|
|
336
|
-
console.warn('No resources — skipping');
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Test first 3 resources max to avoid slow test
|
|
341
|
-
const toTest = resourceFiles.slice(0, 3);
|
|
342
|
-
|
|
343
|
-
for (const res of toTest) {
|
|
344
|
-
const html = await client.getResource(res.layoutid, res.regionid, res.mediaid);
|
|
345
|
-
expect(html, `Resource ${res.mediaid} returned empty`).toBeDefined();
|
|
346
|
-
expect(html.length, `Resource ${res.mediaid} is empty`).toBeGreaterThan(0);
|
|
347
|
-
}
|
|
219
|
+
describe('NotifyStatus', () => {
|
|
220
|
+
it('should report status successfully', async () => {
|
|
221
|
+
const result = await client.notifyStatus({ currentLayoutId: 483 });
|
|
222
|
+
expect(result).toBeDefined();
|
|
223
|
+
expect(result.success).toBe(true);
|
|
348
224
|
});
|
|
349
225
|
});
|
|
350
226
|
|
|
351
|
-
// ────────────────────────────────────────────────────────────────
|
|
352
|
-
// SubmitLog
|
|
353
|
-
// ────────────────────────────────────────────────────────────────
|
|
354
|
-
|
|
355
227
|
describe('SubmitLog', () => {
|
|
356
|
-
it('should submit
|
|
357
|
-
const result = await client.submitLog(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
type: 'info',
|
|
362
|
-
message: 'Integration test log entry',
|
|
363
|
-
},
|
|
364
|
-
]);
|
|
365
|
-
|
|
366
|
-
expect(result).toBeDefined();
|
|
367
|
-
// REST returns { success: true }
|
|
368
|
-
if (typeof result === 'object') {
|
|
369
|
-
expect(result.success).toBe(true);
|
|
370
|
-
}
|
|
228
|
+
it('should submit logs as XML string', async () => {
|
|
229
|
+
const result = await client.submitLog(
|
|
230
|
+
'<logs><log date="2026-03-01" category="info" type="info" message="REST integration test" method="test" /></logs>'
|
|
231
|
+
);
|
|
232
|
+
expect(result).toBe(true);
|
|
371
233
|
});
|
|
372
234
|
|
|
373
|
-
it('should
|
|
374
|
-
const
|
|
375
|
-
date: new Date().toISOString(),
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
message: `Integration test log ${i + 1} of 5`,
|
|
379
|
-
}));
|
|
380
|
-
|
|
381
|
-
const result = await client.submitLog(logs);
|
|
382
|
-
|
|
383
|
-
expect(result).toBeDefined();
|
|
384
|
-
if (typeof result === 'object') {
|
|
385
|
-
expect(result.success).toBe(true);
|
|
386
|
-
}
|
|
235
|
+
it('should submit logs as array', async () => {
|
|
236
|
+
const result = await client.submitLog([
|
|
237
|
+
{ date: new Date().toISOString(), category: 'General', type: 'info', message: 'REST array log test' },
|
|
238
|
+
]);
|
|
239
|
+
expect(result).toBe(true);
|
|
387
240
|
});
|
|
388
241
|
});
|
|
389
242
|
|
|
390
|
-
// ────────────────────────────────────────────────────────────────
|
|
391
|
-
// SubmitStats
|
|
392
|
-
// ────────────────────────────────────────────────────────────────
|
|
393
|
-
|
|
394
243
|
describe('SubmitStats', () => {
|
|
395
|
-
it('should submit proof-of-play stats
|
|
244
|
+
it('should submit proof-of-play stats', async () => {
|
|
396
245
|
const now = new Date();
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
tag: 'integration-test',
|
|
408
|
-
},
|
|
409
|
-
]);
|
|
410
|
-
|
|
411
|
-
expect(result).toBeDefined();
|
|
412
|
-
if (typeof result === 'object') {
|
|
413
|
-
expect(result.success).toBe(true);
|
|
414
|
-
}
|
|
246
|
+
const result = await client.submitStats([{
|
|
247
|
+
type: 'layout',
|
|
248
|
+
fromDt: new Date(now - 60000).toISOString(),
|
|
249
|
+
toDt: now.toISOString(),
|
|
250
|
+
scheduleId: '0',
|
|
251
|
+
layoutId: '483',
|
|
252
|
+
mediaId: '',
|
|
253
|
+
tag: 'rest-integration-test',
|
|
254
|
+
}]);
|
|
255
|
+
expect(result).toBe(true);
|
|
415
256
|
});
|
|
416
257
|
});
|
|
417
258
|
|
|
418
|
-
// ────────────────────────────────────────────────────────────────
|
|
419
|
-
// MediaInventory
|
|
420
|
-
// ────────────────────────────────────────────────────────────────
|
|
421
|
-
|
|
422
259
|
describe('MediaInventory', () => {
|
|
423
|
-
it('should submit
|
|
260
|
+
it('should submit inventory as array', async () => {
|
|
424
261
|
const result = await client.mediaInventory([
|
|
425
|
-
{ id: '1', complete: '1', md5: '
|
|
262
|
+
{ id: '1', complete: '1', md5: 'abf73257821e2cf601d299c509726c03', lastChecked: new Date().toISOString() },
|
|
426
263
|
]);
|
|
427
|
-
|
|
428
264
|
expect(result).toBeDefined();
|
|
429
|
-
|
|
430
|
-
expect(result.success).toBe(true);
|
|
431
|
-
}
|
|
265
|
+
expect(result.success).toBe(true);
|
|
432
266
|
});
|
|
433
267
|
});
|
|
434
268
|
|
|
435
|
-
//
|
|
436
|
-
// Full
|
|
437
|
-
//
|
|
269
|
+
// ──────────────────────────────────────────────────────────────────
|
|
270
|
+
// Full Boot Sequence
|
|
271
|
+
// ──────────────────────────────────────────────────────────────────
|
|
438
272
|
|
|
439
|
-
describe('Full Player Boot
|
|
440
|
-
it('should execute the complete
|
|
441
|
-
// Fresh client with no cache
|
|
273
|
+
describe('Full Player Boot Sequence', () => {
|
|
274
|
+
it('should execute the complete boot sequence', async () => {
|
|
442
275
|
const bootClient = new RestClient({
|
|
443
276
|
cmsUrl: CMS_URL,
|
|
444
277
|
cmsKey: CMS_KEY,
|
|
445
278
|
hardwareKey: HARDWARE_KEY,
|
|
446
279
|
displayName: 'Boot Sequence Test',
|
|
447
|
-
xmrChannel: 'boot-test
|
|
280
|
+
xmrChannel: 'boot-test',
|
|
448
281
|
retryOptions: { maxRetries: 1, baseDelayMs: 500 },
|
|
449
282
|
});
|
|
450
283
|
|
|
451
|
-
//
|
|
452
|
-
const
|
|
453
|
-
expect(
|
|
454
|
-
expect(registration.code).toBeDefined();
|
|
455
|
-
console.log(` Register: ${registration.code}`);
|
|
456
|
-
|
|
457
|
-
if (registration.code !== 'READY') {
|
|
458
|
-
console.warn(' Display not authorized — cannot complete boot sequence');
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
284
|
+
// 1. Register
|
|
285
|
+
const reg = await bootClient.registerDisplay();
|
|
286
|
+
expect(reg.code).toBe('READY');
|
|
461
287
|
|
|
462
|
-
//
|
|
463
|
-
const files = await bootClient.requiredFiles();
|
|
288
|
+
// 2. RequiredFiles
|
|
289
|
+
const { files } = await bootClient.requiredFiles();
|
|
464
290
|
expect(files.length).toBeGreaterThan(0);
|
|
465
|
-
const mediaCount = files.filter(f => f.type === 'media').length;
|
|
466
|
-
const layoutCount = files.filter(f => f.type === 'layout').length;
|
|
467
|
-
const resourceCount = files.filter(f => f.type === 'resource').length;
|
|
468
|
-
console.log(` RequiredFiles: ${files.length} total (${mediaCount} media, ${layoutCount} layouts, ${resourceCount} resources)`);
|
|
469
291
|
|
|
470
|
-
//
|
|
292
|
+
// 3. Schedule
|
|
471
293
|
const schedule = await bootClient.schedule();
|
|
472
294
|
expect(schedule).toBeDefined();
|
|
473
|
-
console.log(` Schedule: ${schedule.layouts.length} layouts, ${schedule.overlays.length} overlays`);
|
|
474
|
-
|
|
475
|
-
// Step 4: GetResource (for first resource if available)
|
|
476
|
-
const resources = files.filter(f => f.type === 'resource');
|
|
477
|
-
if (resources.length > 0) {
|
|
478
|
-
const res = resources[0];
|
|
479
|
-
const html = await bootClient.getResource(res.layoutid, res.regionid, res.mediaid);
|
|
480
|
-
expect(html).toBeDefined();
|
|
481
|
-
console.log(` GetResource: ${html.length} chars for widget ${res.mediaid}`);
|
|
482
|
-
}
|
|
483
295
|
|
|
484
|
-
//
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
296
|
+
// 4. Status
|
|
297
|
+
const status = await bootClient.notifyStatus({ currentLayoutId: schedule.default || 0 });
|
|
298
|
+
expect(status.success).toBe(true);
|
|
299
|
+
|
|
300
|
+
// 5. Log
|
|
301
|
+
const logOk = await bootClient.submitLog([{
|
|
302
|
+
date: new Date().toISOString(), category: 'General', type: 'info',
|
|
303
|
+
message: 'REST boot sequence complete',
|
|
490
304
|
}]);
|
|
491
|
-
expect(
|
|
492
|
-
console.log(` SubmitLog: OK`);
|
|
305
|
+
expect(logOk).toBe(true);
|
|
493
306
|
|
|
494
|
-
//
|
|
307
|
+
// 6. Stats
|
|
495
308
|
const now = new Date();
|
|
496
|
-
const
|
|
497
|
-
type: 'layout',
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
scheduleId: '0',
|
|
501
|
-
layoutId: String(schedule.layouts[0]?.file || '0'),
|
|
502
|
-
mediaId: '',
|
|
503
|
-
tag: 'integration-test-boot',
|
|
309
|
+
const statsOk = await bootClient.submitStats([{
|
|
310
|
+
type: 'layout', fromDt: new Date(now - 10000).toISOString(),
|
|
311
|
+
toDt: now.toISOString(), scheduleId: '0',
|
|
312
|
+
layoutId: String(schedule.default || '0'), mediaId: '', tag: 'boot',
|
|
504
313
|
}]);
|
|
505
|
-
expect(
|
|
506
|
-
console.log(` SubmitStats: OK`);
|
|
507
|
-
|
|
508
|
-
// Step 7: MediaInventory
|
|
509
|
-
const inventory = files.filter(f => f.type === 'media').slice(0, 5).map(f => ({
|
|
510
|
-
id: f.id,
|
|
511
|
-
complete: '1',
|
|
512
|
-
md5: f.md5,
|
|
513
|
-
lastChecked: now.toISOString(),
|
|
514
|
-
}));
|
|
515
|
-
if (inventory.length > 0) {
|
|
516
|
-
const invResult = await bootClient.mediaInventory(inventory);
|
|
517
|
-
expect(invResult).toBeDefined();
|
|
518
|
-
console.log(` MediaInventory: ${inventory.length} items reported`);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
console.log(' Boot sequence: COMPLETE');
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// ────────────────────────────────────────────────────────────────
|
|
526
|
-
// REST vs SOAP Parity
|
|
527
|
-
// ────────────────────────────────────────────────────────────────
|
|
528
|
-
|
|
529
|
-
describe('REST ↔ SOAP Transport Parity', () => {
|
|
530
|
-
it('should expose the same business methods as SOAP transport', () => {
|
|
531
|
-
// Compare only business-level API methods, not transport-specific helpers
|
|
532
|
-
// (REST has restGet/restSend, SOAP has buildEnvelope/call/parseResponse, etc.)
|
|
533
|
-
const businessMethods = [
|
|
534
|
-
'blackList', 'getResource', 'mediaInventory', 'notifyStatus',
|
|
535
|
-
'registerDisplay', 'requiredFiles', 'schedule',
|
|
536
|
-
'submitLog', 'submitScreenShot', 'submitStats',
|
|
537
|
-
].sort();
|
|
538
|
-
|
|
539
|
-
const restMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(client))
|
|
540
|
-
.filter(m => !m.startsWith('_') && m !== 'constructor')
|
|
541
|
-
.sort();
|
|
542
|
-
const soapMethods = Object.getOwnPropertyNames(Object.getPrototypeOf(soapClient))
|
|
543
|
-
.filter(m => !m.startsWith('_') && m !== 'constructor')
|
|
544
|
-
.sort();
|
|
545
|
-
|
|
546
|
-
for (const method of businessMethods) {
|
|
547
|
-
expect(restMethods).toContain(method);
|
|
548
|
-
expect(soapMethods).toContain(method);
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
it('should return same requiredFiles file types and counts', async () => {
|
|
553
|
-
client._etags.clear();
|
|
554
|
-
client._responseCache.clear();
|
|
555
|
-
|
|
556
|
-
const restFiles = await client.requiredFiles();
|
|
557
|
-
const soapFiles = await soapClient.requiredFiles();
|
|
558
|
-
|
|
559
|
-
const restTypes = {};
|
|
560
|
-
for (const f of restFiles) {
|
|
561
|
-
restTypes[f.type] = (restTypes[f.type] || 0) + 1;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
const soapTypes = {};
|
|
565
|
-
for (const f of soapFiles) {
|
|
566
|
-
soapTypes[f.type] = (soapTypes[f.type] || 0) + 1;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
expect(restTypes).toEqual(soapTypes);
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
it('should return same schedule layout IDs', async () => {
|
|
573
|
-
client._etags.clear();
|
|
574
|
-
client._responseCache.clear();
|
|
575
|
-
|
|
576
|
-
const restSchedule = await client.schedule();
|
|
577
|
-
const soapSchedule = await soapClient.schedule();
|
|
578
|
-
|
|
579
|
-
const restIds = restSchedule.layouts.map(l => l.file).sort();
|
|
580
|
-
const soapIds = soapSchedule.layouts.map(l => l.file).sort();
|
|
581
|
-
|
|
582
|
-
expect(restIds).toEqual(soapIds);
|
|
583
|
-
});
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
// ────────────────────────────────────────────────────────────────
|
|
587
|
-
// Error Handling
|
|
588
|
-
// ────────────────────────────────────────────────────────────────
|
|
589
|
-
|
|
590
|
-
describe('Error Handling', () => {
|
|
591
|
-
it('should handle invalid hardware key gracefully', async () => {
|
|
592
|
-
const badClient = new RestClient({
|
|
593
|
-
cmsUrl: CMS_URL,
|
|
594
|
-
cmsKey: CMS_KEY,
|
|
595
|
-
hardwareKey: 'nonexistent-display',
|
|
596
|
-
displayName: 'Bad Display',
|
|
597
|
-
xmrChannel: 'test',
|
|
598
|
-
retryOptions: { maxRetries: 0 },
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
try {
|
|
602
|
-
const result = await badClient.requiredFiles();
|
|
603
|
-
// Some endpoints may return empty rather than error
|
|
604
|
-
expect(Array.isArray(result)).toBe(true);
|
|
605
|
-
} catch (e) {
|
|
606
|
-
// Expected — invalid display
|
|
607
|
-
expect(e.message).toBeDefined();
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
it('should handle unreachable CMS', async () => {
|
|
612
|
-
const badClient = new RestClient({
|
|
613
|
-
cmsUrl: 'https://nonexistent.example.com',
|
|
614
|
-
cmsKey: 'test',
|
|
615
|
-
hardwareKey: 'test',
|
|
616
|
-
displayName: 'Unreachable',
|
|
617
|
-
xmrChannel: 'test',
|
|
618
|
-
retryOptions: { maxRetries: 0 },
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
await expect(badClient.registerDisplay()).rejects.toThrow();
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
// ────────────────────────────────────────────────────────────────
|
|
626
|
-
// Performance & Caching
|
|
627
|
-
// ────────────────────────────────────────────────────────────────
|
|
628
|
-
|
|
629
|
-
describe('Performance & Caching', () => {
|
|
630
|
-
it('should cache requiredFiles ETag across calls', async () => {
|
|
631
|
-
client._etags.clear();
|
|
632
|
-
client._responseCache.clear();
|
|
633
|
-
|
|
634
|
-
// Call 1: fresh fetch
|
|
635
|
-
const t1 = Date.now();
|
|
636
|
-
await client.requiredFiles();
|
|
637
|
-
const firstDuration = Date.now() - t1;
|
|
638
|
-
|
|
639
|
-
// Call 2: should use 304 cache
|
|
640
|
-
const t2 = Date.now();
|
|
641
|
-
await client.requiredFiles();
|
|
642
|
-
const secondDuration = Date.now() - t2;
|
|
643
|
-
|
|
644
|
-
console.log(` First fetch: ${firstDuration}ms, Cached fetch: ${secondDuration}ms`);
|
|
645
|
-
|
|
646
|
-
expect(client._etags.has('/requiredFiles')).toBe(true);
|
|
647
|
-
});
|
|
648
|
-
|
|
649
|
-
it('should cache schedule ETag across calls', async () => {
|
|
650
|
-
client._etags.clear();
|
|
651
|
-
client._responseCache.clear();
|
|
652
|
-
|
|
653
|
-
await client.schedule();
|
|
654
|
-
await client.schedule();
|
|
314
|
+
expect(statsOk).toBe(true);
|
|
655
315
|
|
|
656
|
-
|
|
316
|
+
// 7. Inventory
|
|
317
|
+
const inv = await bootClient.mediaInventory(files.filter(f => f.type === 'media').slice(0, 5).map(f => ({
|
|
318
|
+
id: f.id, complete: '1', md5: f.md5 || '', lastChecked: now.toISOString(),
|
|
319
|
+
})));
|
|
320
|
+
expect(inv.success).toBe(true);
|
|
657
321
|
});
|
|
658
322
|
});
|
|
659
323
|
});
|