fix: contain malformed metadata and stale discovery
This commit is contained in:
@@ -72,6 +72,32 @@ test('malformed hierarchy metadata does not hide valid cameras', () => {
|
||||
assert.equal(payload.diagnostics.sites.error, 'Unavailable');
|
||||
});
|
||||
|
||||
test('repairs lone UTF-16 surrogates without changing valid Unicode or hiding cameras', () => {
|
||||
const payload = projectDeviceHierarchy({
|
||||
devices: [rawDevice(8, {
|
||||
name: 'Front \ud800 camera',
|
||||
model: 'Model \udc00',
|
||||
address: 'Hall \ud800\udc00 / \udc00',
|
||||
server_group_id: 'site-\ud800',
|
||||
device_group_id: 'group-\udc00',
|
||||
})],
|
||||
sites: [{ id: 'site-\ud800', name: 'Valid \ud83d\udcf7 \udc00', pending_deletion_start: '\ud800' }],
|
||||
groups: [{ id: 'group-\udc00', name: 'Group \ud83d\udcf7 \ud800', parent_id: 'site-\ud800' }],
|
||||
metadataErrors: { sites: 'warning \ud800', groups: 'warning \udc00' },
|
||||
});
|
||||
|
||||
assert.equal(payload.devices.length, 1);
|
||||
assert.equal(payload.devices[0].name, 'Front \ufffd camera');
|
||||
assert.equal(payload.devices[0].model, 'Model \ufffd');
|
||||
assert.equal(payload.devices[0].address, 'Hall \ud800\udc00 / \ufffd');
|
||||
assert.equal(payload.devices[0].siteId, 'site-\ufffd');
|
||||
assert.equal(payload.devices[0].deviceGroupId, 'group-\ufffd');
|
||||
assert.deepEqual(payload.sites, [{ id: 'site-\ufffd', name: 'Valid \ud83d\udcf7 \ufffd', pendingDeletionStart: '\ufffd' }]);
|
||||
assert.deepEqual(payload.groups, [{ id: 'group-\ufffd', name: 'Group \ud83d\udcf7 \ufffd', parentId: 'site-\ufffd', pendingDeletionStart: null }]);
|
||||
assert.equal(payload.diagnostics.sites.error, 'warning \ufffd');
|
||||
assert.doesNotMatch(JSON.stringify(payload), /[\ud800-\udfff]/u);
|
||||
});
|
||||
|
||||
test('rejects a projected hierarchy payload above 8 MiB', () => {
|
||||
const devices = Array.from({ length: 10_000 }, (_, index) => rawDevice(index, {
|
||||
name: `Camera ${index} ${'x'.repeat(900)}`,
|
||||
|
||||
@@ -194,6 +194,33 @@ test('tenant and synthetic namespaces stay distinct for reserved and ambiguous I
|
||||
assert.ok([...model.rowByKey.keys()].every((key) => !/\s/.test(key)));
|
||||
});
|
||||
|
||||
test('malformed Unicode metadata builds deterministically and remains searchable and visible', async () => {
|
||||
const fixture = {
|
||||
sites: [{ id: 'site-\ud800', name: 'Site \ud83d\udcf7 \udc00' }],
|
||||
groups: [{ id: 'group-\udc00', name: 'Group \ud83d\udcf7 \ud800', parentId: 'site-\ud800' }],
|
||||
devices: [{
|
||||
id: 'camera-valid', name: 'Lobby \ud800 camera', siteId: 'site-\ud800',
|
||||
deviceGroupId: 'group-\udc00', address: 'Floor \udc00',
|
||||
}],
|
||||
};
|
||||
|
||||
const first = buildDeviceTree(fixture);
|
||||
const second = buildDeviceTree(fixture);
|
||||
assert.equal(first.cameraCount, 1);
|
||||
assert.deepEqual([...first.rowByKey.keys()], [...second.rowByKey.keys()]);
|
||||
assert.doesNotMatch([...first.rowByKey.keys()].join(''), /[\ud800-\udfff]/u);
|
||||
const camera = [...first.cameraByKey.values()][0];
|
||||
assert.equal(camera.name, 'Lobby \ufffd camera');
|
||||
assert.equal(camera.site.name, 'Site \ud83d\udcf7 \ufffd');
|
||||
assert.equal(camera.group.name, 'Group \ud83d\udcf7 \ufffd');
|
||||
assert.equal(camera.source.address, 'Floor \ufffd');
|
||||
assert.doesNotMatch(JSON.stringify(camera.source), /[\ud800-\udfff]/u);
|
||||
assert.match(camera.searchCorpus, /floor \ufffd/);
|
||||
|
||||
const result = await createSearchRunner().search(first, 'lobby \ufffd');
|
||||
assert.deepEqual([...result.cameraKeys], [camera.key]);
|
||||
});
|
||||
|
||||
test('virtual window clamps an obsolete scroll offset after rows shrink', () => {
|
||||
const rows = [{ key: 'only-row' }];
|
||||
const window = calculateVirtualWindow(rows, {
|
||||
|
||||
@@ -162,6 +162,96 @@ test('runtime enforces a bounded whole-discovery deadline', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('newest hierarchy discovery owns the launch allowlist when completions arrive out of order', async () => {
|
||||
const sessionStore = createSessionStore();
|
||||
sessionStore.establish('https://customer.avasecurity.com', 'synthetic-cookie');
|
||||
const deviceA = '550e8400-e29b-41d4-a716-44665544000a';
|
||||
const deviceB = '550e8400-e29b-41d4-a716-44665544000b';
|
||||
const pending = [];
|
||||
const launches = [];
|
||||
const runtime = new AppRuntime({
|
||||
sessionStore,
|
||||
altaClient: {
|
||||
getDevices: () => new Promise((resolve) => pending.push(resolve)),
|
||||
getDeviceSites: async () => [],
|
||||
getDeviceGroups: async () => [],
|
||||
},
|
||||
proxyManager: {
|
||||
launchProxy(request) {
|
||||
launches.push(request.deviceId);
|
||||
return { processId: 5000 + launches.length, deviceId: request.deviceId, status: 'running' };
|
||||
},
|
||||
listTrackedProxies: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const staleA = runtime.getDeviceHierarchy();
|
||||
const freshB = runtime.getDeviceHierarchy();
|
||||
pending[1]([{ guid: deviceB }]);
|
||||
assert.equal((await freshB).success, true);
|
||||
assert.equal((await runtime.launchProxy(deviceB, 'operator@example.com')).success, true);
|
||||
pending[0]([{ guid: deviceA }]);
|
||||
assert.deepEqual(await staleA, {
|
||||
success: false,
|
||||
stale: true,
|
||||
hierarchy: { devices: [], sites: [], groups: [] },
|
||||
message: 'Alta device discovery result is stale',
|
||||
});
|
||||
assert.equal((await runtime.launchProxy(deviceA, 'operator@example.com')).success, false);
|
||||
assert.deepEqual(launches, [deviceB]);
|
||||
});
|
||||
|
||||
test('disconnect and session changes invalidate pending discovery without repopulating the allowlist', async () => {
|
||||
const deviceId = '550e8400-e29b-41d4-a716-44665544000c';
|
||||
for (const invalidate of ['disconnect', 'session-change']) {
|
||||
const sessionStore = createSessionStore();
|
||||
sessionStore.establish('https://first.avasecurity.com', 'synthetic-cookie');
|
||||
let resolveDevices;
|
||||
const runtime = new AppRuntime({
|
||||
sessionStore,
|
||||
altaClient: {
|
||||
getDevices: () => new Promise((resolve) => { resolveDevices = resolve; }),
|
||||
getDeviceSites: async () => [],
|
||||
getDeviceGroups: async () => [],
|
||||
},
|
||||
proxyManager: { listTrackedProxies: () => [] },
|
||||
});
|
||||
const discovery = runtime.getDeviceHierarchy();
|
||||
if (invalidate === 'disconnect') {
|
||||
assert.equal((await runtime.disconnect()).success, true);
|
||||
} else {
|
||||
sessionStore.establish('https://second.avasecurity.com', 'replacement-cookie');
|
||||
runtime.onSessionChanged();
|
||||
}
|
||||
resolveDevices([{ guid: deviceId }]);
|
||||
assert.equal((await discovery).stale, true, invalidate);
|
||||
assert.equal(runtime.allowedDeviceIds.size, 0, invalidate);
|
||||
}
|
||||
});
|
||||
|
||||
test('launch allowlist remains bound to the session origin that produced it', async () => {
|
||||
const deviceId = '550e8400-e29b-41d4-a716-44665544000d';
|
||||
const sessionStore = createSessionStore();
|
||||
sessionStore.establish('https://first.avasecurity.com', 'synthetic-cookie');
|
||||
let launches = 0;
|
||||
const runtime = new AppRuntime({
|
||||
sessionStore,
|
||||
altaClient: {
|
||||
getDevices: async () => [{ guid: deviceId }],
|
||||
getDeviceSites: async () => [],
|
||||
getDeviceGroups: async () => [],
|
||||
},
|
||||
proxyManager: {
|
||||
listTrackedProxies: () => [],
|
||||
launchProxy() { launches += 1; },
|
||||
},
|
||||
});
|
||||
assert.equal((await runtime.getDeviceHierarchy()).success, true);
|
||||
sessionStore.establish('https://second.avasecurity.com', 'replacement-cookie');
|
||||
assert.equal((await runtime.launchProxy(deviceId, 'operator@example.com')).success, false);
|
||||
assert.equal(launches, 0);
|
||||
});
|
||||
|
||||
test('runtime rejects discovery deadlines above 60 seconds', () => {
|
||||
assert.throws(() => new AppRuntime({ discoveryTimeoutMs: 60_001 }), /discovery timeout/i);
|
||||
});
|
||||
@@ -439,6 +529,7 @@ test('preload and renderer expose only narrow, credential-free contracts', () =>
|
||||
assert.doesNotMatch(preload, /launchProxy:\s*\([^)]*(?:cookie|origin)/i);
|
||||
assert.doesNotMatch(renderer, /cookieValue|sessionData\.cookies|cookies\s*:/);
|
||||
assert.match(renderer, /state\.connected\s*&&\s*\(!wasConnected\s*\|\|\s*state\.origin\s*!==\s*previousOrigin\)/);
|
||||
assert.match(read('main.js'), /onConnectionStateChanged:\s*\(\)\s*=>\s*\{\s*runtime\.onSessionChanged\(\)/);
|
||||
assert.doesNotMatch(html, /id="cookieKey"|updateProgress|Install Update/);
|
||||
assert.match(html, /id="altaUsername"/);
|
||||
assert.match(renderer, /openFixedReleasesPage/);
|
||||
|
||||
Reference in New Issue
Block a user