300 lines
10 KiB
TypeScript
300 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import type { FrigateEvent } from '../../src/camera-manager/frigate/types';
|
|
import type { ResolvedMedia } from '../../src/ha/types';
|
|
import { createFrigateEvent } from '../test-utils';
|
|
import {
|
|
createFrigateCameraDescription,
|
|
FakeFrigate,
|
|
FRIGATE_CLIENT_ID,
|
|
getTestFrigateCameraName,
|
|
type FrigateMediaType,
|
|
} from './fake-frigate';
|
|
import type { FakeHASS } from './fake-hass';
|
|
import { CAMERA_ENTITY, createCameraHASS } from './test-utils';
|
|
|
|
const CAMERA_NAME = getTestFrigateCameraName(CAMERA_ENTITY);
|
|
const OTHER_CAMERA_NAME = 'hallway';
|
|
|
|
// Frigate timestamps are Unix seconds. Only their order matters here.
|
|
const EARLIER = 1000;
|
|
const LATER = 2000;
|
|
|
|
interface FrigateFixture {
|
|
hass: FakeHASS;
|
|
frigate: FakeFrigate;
|
|
}
|
|
|
|
const createFrigate = (events: FrigateEvent[]): FrigateFixture => {
|
|
const hass = createCameraHASS([createFrigateCameraDescription()]);
|
|
const frigate = new FakeFrigate(hass);
|
|
frigate.setEvents(events);
|
|
return { hass, frigate };
|
|
};
|
|
|
|
const createEvent = (id: string, event?: Partial<FrigateEvent>): FrigateEvent =>
|
|
createFrigateEvent({
|
|
camera: CAMERA_NAME,
|
|
id,
|
|
start_time: EARLIER,
|
|
end_time: LATER,
|
|
...event,
|
|
});
|
|
|
|
const queryEvents = async (
|
|
hass: FakeHASS,
|
|
parameters?: Record<string, unknown>,
|
|
): Promise<FrigateEvent[]> => {
|
|
const answer = await hass.getHASS().callWS<string>({
|
|
type: 'frigate/events/get',
|
|
instance_id: FRIGATE_CLIENT_ID,
|
|
...parameters,
|
|
});
|
|
const events: FrigateEvent[] = JSON.parse(answer);
|
|
return events;
|
|
};
|
|
|
|
const getEventIDs = async (
|
|
hass: FakeHASS,
|
|
parameters?: Record<string, unknown>,
|
|
): Promise<string[]> => (await queryEvents(hass, parameters)).map((event) => event.id);
|
|
|
|
const createContentID = (
|
|
eventID: string,
|
|
mediaType: FrigateMediaType,
|
|
options?: { clientID?: string; camera?: string },
|
|
): string =>
|
|
[
|
|
'media-source://frigate',
|
|
options?.clientID ?? FRIGATE_CLIENT_ID,
|
|
'event',
|
|
mediaType,
|
|
options?.camera ?? CAMERA_NAME,
|
|
eventID,
|
|
].join('/');
|
|
|
|
const resolveMedia = async (hass: FakeHASS, contentID: string): Promise<ResolvedMedia> =>
|
|
await hass.getHASS().callWS<ResolvedMedia>({
|
|
type: 'media_source/resolve_media',
|
|
media_content_id: contentID,
|
|
});
|
|
|
|
const createThumbnailPath = (eventID: string, clientID = FRIGATE_CLIENT_ID): string =>
|
|
`/api/frigate/${clientID}/thumbnail/${eventID}`;
|
|
|
|
describe('FakeFrigate', () => {
|
|
describe('requests', () => {
|
|
it('should refuse a request meant for another instance', async () => {
|
|
const { hass } = createFrigate([]);
|
|
|
|
await expect(
|
|
hass
|
|
.getHASS()
|
|
.callWS({ type: 'frigate/events/get', instance_id: 'other-frigate' }),
|
|
).rejects.toThrow('FakeFrigate was asked for another instance: other-frigate');
|
|
|
|
await expect(
|
|
hass
|
|
.getHASS()
|
|
.callWS({ type: 'frigate/ptz/info', instance_id: 'other-frigate' }),
|
|
).rejects.toThrow('FakeFrigate was asked for another instance: other-frigate');
|
|
});
|
|
|
|
it('should refuse a request carrying something it does not read', async () => {
|
|
const { hass } = createFrigate([]);
|
|
|
|
await expect(queryEvents(hass, { camera: CAMERA_NAME })).rejects.toThrow(
|
|
'FakeFrigate was sent unknown parameters: camera',
|
|
);
|
|
});
|
|
|
|
it('should refuse a parameter of the wrong type', async () => {
|
|
const { hass } = createFrigate([]);
|
|
|
|
await expect(queryEvents(hass, { cameras: CAMERA_NAME })).rejects.toThrow(
|
|
`FakeFrigate was sent a 'cameras' that is not a list of strings: "${CAMERA_NAME}"`,
|
|
);
|
|
await expect(queryEvents(hass, { has_clip: 'yes' })).rejects.toThrow(
|
|
`FakeFrigate was sent a 'has_clip' that is not true or false: "yes"`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('events', () => {
|
|
it('should return every event when nothing narrows the query', async () => {
|
|
const { hass } = createFrigate([createEvent('one'), createEvent('two')]);
|
|
|
|
expect(await getEventIDs(hass)).toEqual(['one', 'two']);
|
|
});
|
|
|
|
it('should return the newest event first', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('older', { start_time: EARLIER }),
|
|
createEvent('newer', { start_time: LATER }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass)).toEqual(['newer', 'older']);
|
|
});
|
|
|
|
it('should return only the events for the cameras asked for', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('here'),
|
|
createEvent('elsewhere', { camera: OTHER_CAMERA_NAME }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { cameras: [CAMERA_NAME] })).toEqual(['here']);
|
|
});
|
|
|
|
it('should return only the events holding the media asked for', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('clip-only', { has_clip: true, has_snapshot: false }),
|
|
createEvent('snapshot-only', { has_clip: false, has_snapshot: true }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { has_clip: true })).toEqual(['clip-only']);
|
|
expect(await getEventIDs(hass, { has_snapshot: true })).toEqual(['snapshot-only']);
|
|
});
|
|
|
|
it('should return only the events within the period asked for', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('before', { start_time: 100, end_time: 200 }),
|
|
createEvent('during', { start_time: 400, end_time: 600 }),
|
|
createEvent('after', { start_time: 800, end_time: 900 }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { after: 300, before: 700 })).toEqual(['during']);
|
|
});
|
|
|
|
it('should return an event that was still running when the period began', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('running', { start_time: 100, end_time: 500 }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { after: 300 })).toEqual(['running']);
|
|
});
|
|
|
|
it('should return only the events for the requested label', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('person', { label: 'person' }),
|
|
createEvent('car', { label: 'car' }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { labels: ['car'] })).toEqual(['car']);
|
|
});
|
|
|
|
it('should return only the events in the requested zones', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('driveway', { zones: ['driveway'] }),
|
|
createEvent('garden', { zones: ['garden'] }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { zones: ['garden'] })).toEqual(['garden']);
|
|
});
|
|
|
|
it('should return the events carrying any of the sub labels asked for', async () => {
|
|
// Frigate keeps several sub labels on one event as a comma separated
|
|
// string, so asking for one of them has to reach into it.
|
|
const { hass } = createFrigate([
|
|
createEvent('known', { sub_label: 'alice, bob' }),
|
|
createEvent('stranger', { sub_label: null }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { sub_labels: ['bob'] })).toEqual(['known']);
|
|
});
|
|
|
|
it('should return only the favorites', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('kept', { retain_indefinitely: true }),
|
|
createEvent('ordinary', { retain_indefinitely: false }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { favorites: true })).toEqual(['kept']);
|
|
});
|
|
|
|
it('should return no more events than the limit asked for', async () => {
|
|
const { hass } = createFrigate([
|
|
createEvent('older', { start_time: EARLIER }),
|
|
createEvent('newer', { start_time: LATER }),
|
|
]);
|
|
|
|
expect(await getEventIDs(hass, { limit: 1 })).toEqual(['newer']);
|
|
});
|
|
});
|
|
|
|
describe('media', () => {
|
|
it('should resolve a clip to a video and a snapshot to an image', async () => {
|
|
const { hass } = createFrigate([createEvent('one')]);
|
|
|
|
expect(await resolveMedia(hass, createContentID('one', 'clips'))).toEqual({
|
|
url: expect.stringContaining('clip.webm?event=one'),
|
|
mime_type: 'video/webm',
|
|
});
|
|
expect(await resolveMedia(hass, createContentID('one', 'snapshots'))).toEqual({
|
|
url: expect.stringContaining('still-red.png?event=one'),
|
|
mime_type: 'image/png',
|
|
});
|
|
});
|
|
|
|
it('should refuse media for an event it does not have', async () => {
|
|
const { hass } = createFrigate([createEvent('one')]);
|
|
|
|
await expect(resolveMedia(hass, createContentID('two', 'clips'))).rejects.toThrow(
|
|
'FakeFrigate has no such media',
|
|
);
|
|
});
|
|
|
|
it('should refuse media the event does not have', async () => {
|
|
const { hass } = createFrigate([createEvent('one', { has_clip: false })]);
|
|
|
|
await expect(resolveMedia(hass, createContentID('one', 'clips'))).rejects.toThrow(
|
|
'FakeFrigate has no such media',
|
|
);
|
|
});
|
|
|
|
it('should refuse media asked for under the wrong camera or instance', async () => {
|
|
const { hass } = createFrigate([createEvent('one')]);
|
|
|
|
await expect(
|
|
resolveMedia(hass, createContentID('one', 'clips', { camera: 'elsewhere' })),
|
|
).rejects.toThrow('FakeFrigate has no such media');
|
|
await expect(
|
|
resolveMedia(hass, createContentID('one', 'clips', { clientID: 'other' })),
|
|
).rejects.toThrow('FakeFrigate has no such media');
|
|
});
|
|
|
|
it('should honor set media URL', async () => {
|
|
const { hass, frigate } = createFrigate([createEvent('one')]);
|
|
|
|
frigate.setMediaURL('one', 'clips', '/somewhere-else.webm');
|
|
|
|
expect((await resolveMedia(hass, createContentID('one', 'clips'))).url).toBe(
|
|
'/somewhere-else.webm',
|
|
);
|
|
|
|
// The snapshot is a separate media item and is left where it was.
|
|
expect(
|
|
(await resolveMedia(hass, createContentID('one', 'snapshots'))).url,
|
|
).toContain('still-red.png');
|
|
});
|
|
});
|
|
|
|
describe('thumbnails', () => {
|
|
it('should serve a picture for an event', async () => {
|
|
const { hass } = createFrigate([createEvent('one')]);
|
|
|
|
const response = await hass.getHASS().fetchWithAuth(createThumbnailPath('one'));
|
|
|
|
expect(response.ok).toBe(true);
|
|
expect(response.headers.get('Content-Type')).toContain('image/png');
|
|
});
|
|
|
|
it('should refuse a thumbnail for an event it does not have', async () => {
|
|
const { hass } = createFrigate([createEvent('one')]);
|
|
|
|
await expect(
|
|
hass.getHASS().fetchWithAuth(createThumbnailPath('two')),
|
|
).rejects.toThrow('FakeFrigate has no thumbnail at');
|
|
});
|
|
});
|
|
});
|