90 lines
5.6 KiB
TypeScript
90 lines
5.6 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { HikvisionClient } from '../../ts/integrations/hikvision/index.js';
|
|
|
|
tap.test('fetches Hikvision ISAPI snapshots and maps NVR channels/events', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const requests: string[] = [];
|
|
globalThis.fetch = (async (inputArg: RequestInfo | URL) => {
|
|
const url = typeof inputArg === 'string' ? inputArg : inputArg instanceof URL ? inputArg.toString() : inputArg.url;
|
|
requests.push(url);
|
|
if (url.includes('/ISAPI/System/deviceInfo')) {
|
|
return new Response('<DeviceInfo><deviceName>Garage NVR</deviceName><deviceID>HKV123</deviceID><model>DS-7608NI</model><serialNumber>HKV123</serialNumber><macAddress>00:11:22:33:44:55</macAddress><firmwareVersion>V5.7</firmwareVersion><deviceType>NVR</deviceType></DeviceInfo>', { headers: { 'content-type': 'application/xml' } });
|
|
}
|
|
if (url.includes('/ISAPI/Streaming/channels/101/picture')) {
|
|
return new Response(new Uint8Array([0xff, 0xd8, 0xff]), { headers: { 'content-type': 'image/jpeg' } });
|
|
}
|
|
if (url.includes('/ISAPI/Streaming/channels')) {
|
|
return new Response('<StreamingChannelList><StreamingChannel><id>101</id><channelName>Front Door</channelName><enabled>true</enabled></StreamingChannel><StreamingChannel><id>102</id><channelName>Front Door sub</channelName><enabled>true</enabled></StreamingChannel><StreamingChannel><id>201</id><channelName>Driveway</channelName><enabled>true</enabled></StreamingChannel></StreamingChannelList>', { headers: { 'content-type': 'application/xml' } });
|
|
}
|
|
if (url.includes('/ISAPI/Event/triggers')) {
|
|
return new Response('<EventTriggerList><EventTrigger><id>rule1</id><eventType>linedetection</eventType><eventDescription>Line</eventDescription><EventTriggerNotificationList><EventTriggerNotification><notificationMethod>center</notificationMethod><videoInputChannelID>1</videoInputChannelID></EventTriggerNotification></EventTriggerNotificationList></EventTrigger><EventTrigger><id>rule2</id><eventType>fielddetection</eventType><eventDescription>Field</eventDescription><EventTriggerNotificationList><EventTriggerNotification><notificationMethod>record</notificationMethod><videoInputChannelID>2</videoInputChannelID></EventTriggerNotification></EventTriggerNotificationList></EventTrigger></EventTriggerList>', { headers: { 'content-type': 'application/xml' } });
|
|
}
|
|
return new Response('Not Found', { status: 404 });
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const client = new HikvisionClient({ host: '192.168.1.40', username: 'admin', password: 'secret' });
|
|
const snapshot = await client.getSnapshot();
|
|
expect(snapshot.connected).toBeTrue();
|
|
expect(snapshot.deviceInfo.deviceType).toEqual('NVR');
|
|
expect(snapshot.deviceInfo.macAddress).toEqual('001122334455');
|
|
expect(snapshot.cameras.length).toEqual(2);
|
|
expect(snapshot.cameras[0].name).toEqual('Front Door');
|
|
expect(snapshot.binarySensors.find((sensorArg) => sensorArg.key === 'line_crossing' && sensorArg.channel === 1)?.sensorType).toEqual('Line Crossing');
|
|
expect(snapshot.binarySensors.find((sensorArg) => sensorArg.key === 'field_detection' && sensorArg.channel === 2)?.sensorType).toEqual('Field Detection');
|
|
expect(snapshot.cameras[0].rtspUrl).toEqual('rtsp://admin:secret@192.168.1.40:554/Streaming/Channels/101');
|
|
|
|
const image = await client.execute({ type: 'snapshot_image', service: 'snapshot', cameraId: '1' });
|
|
expect((image as { contentType: string }).contentType).toEqual('image/jpeg');
|
|
expect((image as { dataBase64: string }).dataBase64).toEqual('/9j/');
|
|
expect(requests.some((requestArg) => requestArg.includes('/ISAPI/Streaming/channels/101/picture'))).toBeTrue();
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
tap.test('does not pretend snapshot commands succeed without a live HTTP executor', async () => {
|
|
const clientWithoutHost = new HikvisionClient({});
|
|
let missingHostError = '';
|
|
try {
|
|
await clientWithoutHost.execute({ type: 'snapshot_image', service: 'snapshot' });
|
|
} catch (errorArg) {
|
|
missingHostError = errorArg instanceof Error ? errorArg.message : String(errorArg);
|
|
}
|
|
expect(missingHostError.includes('host is required')).toBeTrue();
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = (async (inputArg: RequestInfo | URL) => {
|
|
const url = typeof inputArg === 'string' ? inputArg : inputArg instanceof URL ? inputArg.toString() : inputArg.url;
|
|
if (url.includes('/ISAPI/System/deviceInfo')) {
|
|
return new Response('<DeviceInfo><deviceName>Camera</deviceName><serialNumber>HKV789</serialNumber><deviceType>Camera</deviceType></DeviceInfo>');
|
|
}
|
|
if (url.includes('/ISAPI/Streaming/channels/101/picture')) {
|
|
return new Response('busy', { status: 503 });
|
|
}
|
|
if (url.includes('/ISAPI/Streaming/channels')) {
|
|
return new Response('<StreamingChannelList><StreamingChannel><id>101</id><channelName>Camera</channelName><enabled>true</enabled></StreamingChannel></StreamingChannelList>');
|
|
}
|
|
if (url.includes('/ISAPI/Event/triggers')) {
|
|
return new Response('<EventTriggerList/>');
|
|
}
|
|
return new Response('', { status: 404 });
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const client = new HikvisionClient({ host: '192.168.1.42' });
|
|
await client.getSnapshot();
|
|
let commandError = '';
|
|
try {
|
|
await client.execute({ type: 'snapshot_image', service: 'snapshot', cameraId: '1' });
|
|
} catch (errorArg) {
|
|
commandError = errorArg instanceof Error ? errorArg.message : String(errorArg);
|
|
}
|
|
expect(commandError.includes('failed with HTTP 503')).toBeTrue();
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
export default tap.start();
|