Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions __tests__/accessrules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { Onvif } from '../src';
import { AccessProfile } from '../src/interfaces/accessrules';

const ACCESS_PROFILE_TOKEN_1 = 'AccessProfileToken_1';
const ACCESS_POINT_TOKEN_1 = 'AccessPointToken_1';

let cam: Onvif;
const createdAccessProfileTokens: string[] = [];

beforeAll(async () => {
cam = new Onvif({
hostname: '127.0.0.1',
username: 'admin',
password: 'admin',
port: 8000,
});
await cam.connect();
});

afterEach(async () => {
while (createdAccessProfileTokens.length > 0) {
const token = createdAccessProfileTokens.pop()!;
try {
await cam.accessRules.deleteAccessProfile({ token });
} catch {
// already deleted
}
}
});

describe('AccessRules', () => {
beforeAll(() => {
if (!cam.uri.accessrules) {
throw new Error('AccessRules service is not available on the test device');
}
});

describe('getServiceCapabilities', () => {
it('should return access rules service capabilities as an object', async () => {
const caps = await cam.accessRules.getServiceCapabilities();
expect(caps).toBeDefined();
expect(typeof caps).toBe('object');
expect(Array.isArray(caps)).toBe(false);
});

it('should return capability flags from the happytime mock server', async () => {
const caps = await cam.accessRules.getServiceCapabilities();
expect(caps.maxLimit).toBe(10);
expect(caps.maxAccessProfiles).toBe(10);
expect(caps.maxAccessPoliciesPerAccessProfile).toBe(1);
expect(caps.multipleSchedulesPerAccessPointSupported).toBe(true);
expect(caps.clientSuppliedTokenSupported).toBe(true);
});
});

describe('getAccessProfileInfoList / getAccessProfileInfo', () => {
it('should return access profile info list from the mock server', async () => {
const list = await cam.accessRules.getAccessProfileInfoList();
expect(list.accessProfileInfo?.length).toBeGreaterThanOrEqual(1);
expect(list.accessProfileInfo?.[0]).toHaveProperty('token');
expect(list.accessProfileInfo?.[0]).toHaveProperty('name');
expect(list.accessProfileInfo?.[0].token).toBe(ACCESS_PROFILE_TOKEN_1);
});

it('should return access profile info for requested tokens', async () => {
const response = await cam.accessRules.getAccessProfileInfo({
token: [ACCESS_PROFILE_TOKEN_1],
});
expect(response.accessProfileInfo?.length).toBe(1);
expect(response.accessProfileInfo?.[0].token).toBe(ACCESS_PROFILE_TOKEN_1);
expect(response.accessProfileInfo?.[0].name).toBe('AccessProfileName_1');
expect(response.accessProfileInfo?.[0].description).toBe('test');
});
});

describe('getAccessProfileList / getAccessProfiles', () => {
it('should return access profile list from the mock server', async () => {
const list = await cam.accessRules.getAccessProfileList();
expect(list.accessProfile?.length).toBeGreaterThanOrEqual(1);
expect(list.accessProfile?.[0].accessPolicy?.length).toBeGreaterThanOrEqual(1);
expect(list.accessProfile?.[0].accessPolicy?.[0].entity).toBe(ACCESS_POINT_TOKEN_1);
});

it('should return access profiles for requested tokens', async () => {
const response = await cam.accessRules.getAccessProfiles({
token: [ACCESS_PROFILE_TOKEN_1],
});
expect(response.accessProfile?.length).toBe(1);
expect(response.accessProfile?.[0].token).toBe(ACCESS_PROFILE_TOKEN_1);
expect(response.accessProfile?.[0].name).toBe('AccessProfileName_1');
expect(response.accessProfile?.[0].accessPolicy?.[0].scheduleToken).toBe('test');
});

it('should return empty list for an unknown token', async () => {
const response = await cam.accessRules.getAccessProfiles({ token: ['InvalidToken'] });
expect(response.accessProfile ?? []).toHaveLength(0);
});
});

describe('createAccessProfile / modifyAccessProfile / deleteAccessProfile', () => {
it('should create, modify, and delete an access profile', async () => {
const accessProfile: AccessProfile = {
token: '',
name: 'TempAccessProfile',
description: 'temp profile',
accessPolicy: [
{
scheduleToken: 'ScheduleToken_1',
entity: ACCESS_POINT_TOKEN_1,
},
],
};

const token = await cam.accessRules.createAccessProfile({ accessProfile });
createdAccessProfileTokens.push(token);
expect(token).toBeDefined();

await cam.accessRules.modifyAccessProfile({
accessProfile: {
...accessProfile,
token,
name: 'TempAccessProfileModified',
description: 'modified',
},
});

const response = await cam.accessRules.getAccessProfiles({ token: [token] });
expect(response.accessProfile?.[0].name).toBe('TempAccessProfileModified');
expect(response.accessProfile?.[0].description).toBe('modified');
expect(response.accessProfile?.[0].accessPolicy?.[0].entity).toBe(ACCESS_POINT_TOKEN_1);

await cam.accessRules.deleteAccessProfile({ token });
createdAccessProfileTokens.pop();

const afterDelete = await cam.accessRules.getAccessProfiles({ token: [token] });
expect(afterDelete.accessProfile ?? []).toHaveLength(0);
});
});

describe('setAccessProfile', () => {
it('should create or replace an access profile with a client-supplied token', async () => {
const token = 'ClientSuppliedAccessProfileToken';
createdAccessProfileTokens.push(token);

await cam.accessRules.setAccessProfile({
accessProfile: {
token,
name: 'SetAccessProfile',
description: 'set via client token',
accessPolicy: [
{
scheduleToken: 'ScheduleToken_1',
entity: ACCESS_POINT_TOKEN_1,
},
],
},
});

const response = await cam.accessRules.getAccessProfiles({ token: [token] });
expect(response.accessProfile?.[0].token).toBe(token);
expect(response.accessProfile?.[0].name).toBe('SetAccessProfile');
});
});
});
167 changes: 167 additions & 0 deletions src/accessrules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* AccessRules ver10 module
* @author Andrew D.Laptev <a.d.laptev@gmail.com>
* @see https://www.onvif.org/ver10/accessrules/wsdl/accessrules.wsdl
*/

import { Onvif } from './onvif';
import Service from './service';
import {
AccessPolicy,
AccessProfile,
AccessProfileInfo,
Capabilities,
CreateAccessProfile,
CreateAccessProfileResponse,
DeleteAccessProfile,
GetAccessProfileInfo,
GetAccessProfileInfoList,
GetAccessProfileInfoListResponse,
GetAccessProfileInfoResponse,
GetAccessProfileList,
GetAccessProfileListResponse,
GetAccessProfiles,
GetAccessProfilesResponse,
ModifyAccessProfile,
SetAccessProfile,
} from './interfaces/accessrules';

/**
* AccessRules service
* @example
* ```ts
* const list = await cam.accessRules.getAccessProfileInfoList();
* const token = list.accessProfileInfo![0].token;
* console.log((await cam.accessRules.getAccessProfiles({ token: [token] })).accessProfile);
* ```
*/
export default class AccessRules extends Service {
constructor(onvif: Onvif) {
super(onvif, 'accessrules');
}

private static accessPolicyToBuild(policy: AccessPolicy) {
return {
ScheduleToken: policy.scheduleToken,
Entity: policy.entity,
...(policy.entityType !== undefined && { EntityType: policy.entityType }),
...(policy.extension && { Extension: policy.extension }),
};
}

private static accessProfileInfoToBuild(profile: AccessProfileInfo | AccessProfile) {
return {
$: { token: profile.token },
Name: profile.name,
...(profile.description && { Description: profile.description }),
};
}

private static accessProfileToBuild(profile: AccessProfile) {
return {
...AccessRules.accessProfileInfoToBuild(profile),
...(profile.accessPolicy && {
AccessPolicy: profile.accessPolicy.map(AccessRules.accessPolicyToBuild),
}),
...(profile.extension && { Extension: profile.extension }),
};
}

/**
* Returns the capabilities of the access rules service.
*/
async getServiceCapabilities(): Promise<Capabilities> {
const response = await this.request({ GetServiceCapabilities: {} });
return response.getServiceCapabilitiesResponse?.capabilities ?? {};
}

/**
* Returns access profile info items for the requested tokens.
* @param options
*/
async getAccessProfileInfo({ token }: GetAccessProfileInfo): Promise<GetAccessProfileInfoResponse> {
const response = await this.request({ GetAccessProfileInfo: { Token: token } }, { array: ['accessProfileInfo'] });
return response.getAccessProfileInfoResponse ?? {};
}

/**
* Returns a list of access profile info items.
* @param options
*/
async getAccessProfileInfoList(options: GetAccessProfileInfoList = {}): Promise<GetAccessProfileInfoListResponse> {
const response = await this.request(
{
GetAccessProfileInfoList: {
...(options.limit !== undefined && { Limit: options.limit }),
...(options.startReference && { StartReference: options.startReference }),
},
},
{ array: ['accessProfileInfo'] },
);
return response.getAccessProfileInfoListResponse ?? {};
}

/**
* Returns access profile items for the requested tokens.
* @param options
*/
async getAccessProfiles({ token }: GetAccessProfiles): Promise<GetAccessProfilesResponse> {
const response = await this.request(
{ GetAccessProfiles: { Token: token } },
{ array: ['accessProfile', 'accessPolicy'] },
);
return response.getAccessProfilesResponse ?? {};
}

/**
* Returns a list of access profile items.
* @param options
*/
async getAccessProfileList(options: GetAccessProfileList = {}): Promise<GetAccessProfileListResponse> {
const response = await this.request(
{
GetAccessProfileList: {
...(options.limit !== undefined && { Limit: options.limit }),
...(options.startReference && { StartReference: options.startReference }),
},
},
{ array: ['accessProfile', 'accessPolicy'] },
);
return response.getAccessProfileListResponse ?? {};
}

/**
* Creates a new access profile.
* @param options
*/
async createAccessProfile({ accessProfile }: CreateAccessProfile): Promise<CreateAccessProfileResponse['token']> {
const response = await this.request({
CreateAccessProfile: { AccessProfile: AccessRules.accessProfileToBuild(accessProfile) },
});
return response.createAccessProfileResponse.token;
}

/**
* Creates or replaces an access profile (requires ClientSuppliedTokenSupported).
* @param options
*/
async setAccessProfile({ accessProfile }: SetAccessProfile): Promise<void> {
await this.request({ SetAccessProfile: { AccessProfile: AccessRules.accessProfileToBuild(accessProfile) } });
}

/**
* Modifies an existing access profile.
* @param options
*/
async modifyAccessProfile({ accessProfile }: ModifyAccessProfile): Promise<void> {
await this.request({ ModifyAccessProfile: { AccessProfile: AccessRules.accessProfileToBuild(accessProfile) } });
}

/**
* Deletes an access profile.
* @param options
*/
async deleteAccessProfile({ token }: DeleteAccessProfile): Promise<void> {
await this.request({ DeleteAccessProfile: { Token: token } });
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export { default as Recording } from './recording';
export { default as DoorControl } from './doorcontrol';
export { default as AccessControl } from './accesscontrol';
export { default as Credential } from './credential';
export { default as AccessRules } from './accessrules';
export { default as Thermal } from './thermal';
export { default as Analytics } from './analytics';
export { default as DeviceIO } from './deviceio';
Expand Down
12 changes: 12 additions & 0 deletions src/onvif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type Recording from './recording';
import type DoorControl from './doorcontrol';
import type AccessControl from './accesscontrol';
import type Credential from './credential';
import type AccessRules from './accessrules';
import type Thermal from './thermal';
import type Analytics from './analytics';
import type DeviceIO from './deviceio';
Expand Down Expand Up @@ -77,6 +78,7 @@ export interface OnvifServices {
doorcontrol?: URL;
accesscontrol?: URL;
credential?: URL;
accessrules?: URL;
thermal?: URL;
actionengine?: URL;
search?: URL;
Expand Down Expand Up @@ -345,6 +347,15 @@ export class Onvif extends EventEmitter<OnvifEvents> {
* ```
*/
public readonly credential: Credential;
/**
* AccessRules namespace for accessrules v1.0 methods
* @example
* ```typescript
* const list = await onvif.accessRules.getAccessProfileInfoList();
* console.log(list);
* ```
*/
public readonly accessRules: AccessRules;
/**
* Thermal namespace for thermal v1.0 methods
* @example
Expand Down Expand Up @@ -495,6 +506,7 @@ export class Onvif extends EventEmitter<OnvifEvents> {
this.doorControl = createLazy<DoorControl>(this, () => import('./doorcontrol'));
this.accessControl = createLazy<AccessControl>(this, () => import('./accesscontrol'));
this.credential = createLazy<Credential>(this, () => import('./credential'));
this.accessRules = createLazy<AccessRules>(this, () => import('./accessrules'));
this.thermal = createLazy<Thermal>(this, () => import('./thermal'));
this.analytics = createLazy<Analytics>(this, () => import('./analytics'));
this.deviceIO = createLazy<DeviceIO>(this, () => import('./deviceio'));
Expand Down
1 change: 1 addition & 0 deletions src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const XMLNS: Record<keyof OnvifServices, string> = {
doorcontrol: 'http://www.onvif.org/ver10/doorcontrol/wsdl',
accesscontrol: 'http://www.onvif.org/ver10/accesscontrol/wsdl',
credential: 'http://www.onvif.org/ver10/credential/wsdl',
accessrules: 'http://www.onvif.org/ver10/accessrules/wsdl',
thermal: 'http://www.onvif.org/ver10/thermal/wsdl',
search: 'http://www.onvif.org/ver10/search/wsdl',
analyticsdevice: 'http://www.onvif.org/ver10/analyticsdevice/wsdl',
Expand Down
Loading