@matterbridge/test-utils 3.10.7-dev-20260823-f911eb8
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/dist/buntest/export.d.ts +25 -0
- package/dist/buntest/export.d.ts.map +1 -0
- package/dist/buntest/export.js +26 -0
- package/dist/buntest/export.js.map +1 -0
- package/dist/buntest/matterTest.d.ts +351 -0
- package/dist/buntest/matterTest.d.ts.map +1 -0
- package/dist/buntest/matterTest.js +722 -0
- package/dist/buntest/matterTest.js.map +1 -0
- package/dist/buntest/setupTest.d.ts +84 -0
- package/dist/buntest/setupTest.d.ts.map +1 -0
- package/dist/buntest/setupTest.js +153 -0
- package/dist/buntest/setupTest.js.map +1 -0
- package/dist/export.d.ts +25 -0
- package/dist/export.d.ts.map +1 -0
- package/dist/export.js +25 -0
- package/dist/export.js.map +1 -0
- package/dist/flushAsync.d.ts +36 -0
- package/dist/flushAsync.d.ts.map +1 -0
- package/dist/flushAsync.js +43 -0
- package/dist/flushAsync.js.map +1 -0
- package/dist/jest/export.d.ts +25 -0
- package/dist/jest/export.d.ts.map +1 -0
- package/dist/jest/export.js +26 -0
- package/dist/jest/export.js.map +1 -0
- package/dist/jest/matterTest.d.ts +351 -0
- package/dist/jest/matterTest.d.ts.map +1 -0
- package/dist/jest/matterTest.js +722 -0
- package/dist/jest/matterTest.js.map +1 -0
- package/dist/jest/setupTest.d.ts +83 -0
- package/dist/jest/setupTest.d.ts.map +1 -0
- package/dist/jest/setupTest.js +154 -0
- package/dist/jest/setupTest.js.map +1 -0
- package/dist/logKeepAlives.d.ts +32 -0
- package/dist/logKeepAlives.d.ts.map +1 -0
- package/dist/logKeepAlives.js +69 -0
- package/dist/logKeepAlives.js.map +1 -0
- package/dist/matter/export.d.ts +24 -0
- package/dist/matter/export.d.ts.map +1 -0
- package/dist/matter/export.js +25 -0
- package/dist/matter/export.js.map +1 -0
- package/dist/matter/matterRequest.d.ts +257 -0
- package/dist/matter/matterRequest.d.ts.map +1 -0
- package/dist/matter/matterRequest.js +455 -0
- package/dist/matter/matterRequest.js.map +1 -0
- package/dist/vitest/export.d.ts +25 -0
- package/dist/vitest/export.d.ts.map +1 -0
- package/dist/vitest/export.js +26 -0
- package/dist/vitest/export.js.map +1 -0
- package/dist/vitest/matterTest.d.ts +351 -0
- package/dist/vitest/matterTest.d.ts.map +1 -0
- package/dist/vitest/matterTest.js +723 -0
- package/dist/vitest/matterTest.js.map +1 -0
- package/dist/vitest/setupTest.d.ts +62 -0
- package/dist/vitest/setupTest.d.ts.map +1 -0
- package/dist/vitest/setupTest.js +129 -0
- package/dist/vitest/setupTest.js.map +1 -0
- package/package.json +97 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file packages/jest-utils/src/jestMatterTest.ts
|
|
3
|
+
* @description This file contains the Jest Matter Test Environment.
|
|
4
|
+
* @author Luca Liguori
|
|
5
|
+
* @created 2026-04-19
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
* @license Apache-2.0
|
|
8
|
+
*
|
|
9
|
+
* Copyright 2025, 2026, 2027 Luca Liguori.
|
|
10
|
+
*
|
|
11
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
12
|
+
* you may not use this file except in compliance with the License.
|
|
13
|
+
* You may obtain a copy of the License at
|
|
14
|
+
*
|
|
15
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
16
|
+
*
|
|
17
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
18
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
19
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
20
|
+
* See the License for the specific language governing permissions and
|
|
21
|
+
* limitations under the License.
|
|
22
|
+
*/
|
|
23
|
+
// oxlint-disable-next-line import/no-unassigned-import
|
|
24
|
+
import '@matter/nodejs'; // Set up Node.js environment for matter.js
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
// @matter
|
|
27
|
+
import { Environment, Lifecycle, LogFormat as MatterLogFormat, LogLevel as MatterLogLevel } from '@matter/general';
|
|
28
|
+
import { Endpoint, ServerNode, ServerNodeStore } from '@matter/node';
|
|
29
|
+
import { BridgedDeviceBasicInformationServer } from '@matter/node/behaviors/bridged-device-basic-information';
|
|
30
|
+
import { DescriptorServer } from '@matter/node/behaviors/descriptor';
|
|
31
|
+
import { OnOffServer } from '@matter/node/behaviors/on-off';
|
|
32
|
+
import { MountedOnOffControlDevice } from '@matter/node/devices/mounted-on-off-control';
|
|
33
|
+
import { AggregatorEndpoint } from '@matter/node/endpoints';
|
|
34
|
+
import { DeviceTypeId, VendorId } from '@matter/types/datatype';
|
|
35
|
+
// @matterbridge
|
|
36
|
+
import { MATTER_STORAGE_DIR } from '@matterbridge/types';
|
|
37
|
+
import { inspectError } from '@matterbridge/utils/error';
|
|
38
|
+
// node-ansi-logger module
|
|
39
|
+
import { er, rs } from 'node-ansi-logger';
|
|
40
|
+
// local modules
|
|
41
|
+
import { flushAsync } from '../flushAsync.js';
|
|
42
|
+
import { HOMEDIR, log, NAME } from './setupTest.js';
|
|
43
|
+
export let environment;
|
|
44
|
+
export let server;
|
|
45
|
+
export let aggregator;
|
|
46
|
+
const noop = () => undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Create a matter test environment for testing:
|
|
49
|
+
* - it will remove any existing home directory
|
|
50
|
+
* - setup the matter environment with name, debug logging and ANSI format
|
|
51
|
+
*
|
|
52
|
+
* @returns {Environment} - The default matter environment.
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const NAME = 'BaseTest';
|
|
56
|
+
* const MATTER_PORT = 8000;
|
|
57
|
+
* const MATTER_CREATE_ONLY = true;
|
|
58
|
+
*
|
|
59
|
+
* // Setup the test environment
|
|
60
|
+
* await setupTest(NAME, false);
|
|
61
|
+
*
|
|
62
|
+
* // Setup the Matter test environment
|
|
63
|
+
* await createTestEnvironment();
|
|
64
|
+
*
|
|
65
|
+
* // Create the server node and aggregator
|
|
66
|
+
* await createServerNode(MATTER_PORT);
|
|
67
|
+
*
|
|
68
|
+
* // Start the server node if not in create-only mode
|
|
69
|
+
* if (!MATTER_CREATE_ONLY) await startServerNode();
|
|
70
|
+
*
|
|
71
|
+
* // Stop or flush the server node depending on the create-only mode
|
|
72
|
+
* if (MATTER_CREATE_ONLY) await flushServerNode();
|
|
73
|
+
* else await stopServerNode();
|
|
74
|
+
*
|
|
75
|
+
* // Destroy the Matter test environment
|
|
76
|
+
* await destroyTestEnvironment();
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
// oxlint-disable-next-line typescript/require-await
|
|
80
|
+
export async function createTestEnvironment() {
|
|
81
|
+
expect(NAME).toBeDefined();
|
|
82
|
+
expect(typeof NAME).toBe('string');
|
|
83
|
+
expect(NAME.length).toBeGreaterThanOrEqual(4); // avoid accidental deletion of short paths like "/" or "C:\"
|
|
84
|
+
// Setup the matter environment
|
|
85
|
+
environment = Environment.default;
|
|
86
|
+
environment.vars.set('log.level', MatterLogLevel.DEBUG);
|
|
87
|
+
environment.vars.set('log.format', MatterLogFormat.ANSI);
|
|
88
|
+
environment.vars.set('path.root', path.join(HOMEDIR, '.matterbridge', MATTER_STORAGE_DIR));
|
|
89
|
+
environment.vars.set('runtime.signals', false);
|
|
90
|
+
environment.vars.set('runtime.exitcode', false);
|
|
91
|
+
return environment;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Destroy the matter test environment
|
|
95
|
+
*
|
|
96
|
+
* @returns {Promise<void>} A promise that resolves when the test environment is destroyed.
|
|
97
|
+
* @example
|
|
98
|
+
* ```typescript
|
|
99
|
+
* const NAME = 'BaseTest';
|
|
100
|
+
* const MATTER_PORT = 8000;
|
|
101
|
+
* const MATTER_CREATE_ONLY = true;
|
|
102
|
+
*
|
|
103
|
+
* // Setup the test environment
|
|
104
|
+
* await setupTest(NAME, false);
|
|
105
|
+
*
|
|
106
|
+
* // Setup the Matter test environment
|
|
107
|
+
* await createTestEnvironment();
|
|
108
|
+
*
|
|
109
|
+
* // Create the server node and aggregator
|
|
110
|
+
* await createServerNode(MATTER_PORT);
|
|
111
|
+
*
|
|
112
|
+
* // Start the server node if not in create-only mode
|
|
113
|
+
* if (!MATTER_CREATE_ONLY) await startServerNode();
|
|
114
|
+
*
|
|
115
|
+
* // Stop or flush the server node depending on the create-only mode
|
|
116
|
+
* if (MATTER_CREATE_ONLY) await flushServerNode();
|
|
117
|
+
* else await stopServerNode();
|
|
118
|
+
*
|
|
119
|
+
* // Destroy the Matter test environment
|
|
120
|
+
* await destroyTestEnvironment();
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export async function destroyTestEnvironment() {
|
|
124
|
+
// Nothing to clean up right now
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get a mocked PlatformMatterbridge.
|
|
128
|
+
*
|
|
129
|
+
* @returns {PlatformMatterbridge} An object representing the mocked PlatformMatterbridge.
|
|
130
|
+
*/
|
|
131
|
+
export function getMatterbridge() {
|
|
132
|
+
const matterbridge = {
|
|
133
|
+
systemInformation: {
|
|
134
|
+
interfaceName: 'eth0',
|
|
135
|
+
macAddress: 'aa:bb:cc:dd:ee:ff',
|
|
136
|
+
ipv4Address: '192.168.68.100',
|
|
137
|
+
ipv6Address: 'fd78:cbf8:4939:46e2:51b2:2163:7f88:c33d',
|
|
138
|
+
nodeVersion: '24.16.0',
|
|
139
|
+
hostname: 'matterbridge',
|
|
140
|
+
user: 'jest',
|
|
141
|
+
osType: 'Linux',
|
|
142
|
+
osRelease: '6.8.0-xxxx',
|
|
143
|
+
osPlatform: 'linux',
|
|
144
|
+
osArch: 'arm64',
|
|
145
|
+
totalMemory: '0 B',
|
|
146
|
+
freeMemory: '0 B',
|
|
147
|
+
systemUptime: '0s',
|
|
148
|
+
processUptime: '0s',
|
|
149
|
+
cpuUsage: '0%',
|
|
150
|
+
processCpuUsage: '0%',
|
|
151
|
+
rss: '0 B',
|
|
152
|
+
heapTotal: '0 B',
|
|
153
|
+
heapUsed: '0 B',
|
|
154
|
+
},
|
|
155
|
+
uuid: '00000000-0000-0000-0000-000000000000',
|
|
156
|
+
rootDirectory: HOMEDIR,
|
|
157
|
+
homeDirectory: HOMEDIR,
|
|
158
|
+
matterbridgeDirectory: path.join(HOMEDIR, '.matterbridge'),
|
|
159
|
+
matterbridgePluginDirectory: path.join(HOMEDIR, 'Matterbridge'),
|
|
160
|
+
matterbridgeCertDirectory: path.join(HOMEDIR, '.mattercert'),
|
|
161
|
+
globalModulesDirectory: path.join(HOMEDIR, 'node_modules'),
|
|
162
|
+
matterbridgeVersion: '3.10.7',
|
|
163
|
+
matterbridgeLatestVersion: '3.10.7',
|
|
164
|
+
matterbridgeDevVersion: '3.10.7',
|
|
165
|
+
frontendVersion: '3.5.9',
|
|
166
|
+
bridgeMode: 'none',
|
|
167
|
+
restartMode: 'none',
|
|
168
|
+
virtualMode: 'mounted_switch',
|
|
169
|
+
aggregatorVendorId: VendorId(0xfff1),
|
|
170
|
+
aggregatorVendorName: 'Matterbridge',
|
|
171
|
+
aggregatorProductId: 0x8000,
|
|
172
|
+
aggregatorProductName: 'Matterbridge Jest',
|
|
173
|
+
};
|
|
174
|
+
return matterbridge;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Add a bridged endpoint
|
|
178
|
+
*
|
|
179
|
+
* @param {string} pluginName The name of the plugin.
|
|
180
|
+
* @param {Endpoint} device The device to add.
|
|
181
|
+
* @returns {Promise<boolean>} A promise that resolves to true if the endpoint was added successfully, or rejects with an error if the operation failed.
|
|
182
|
+
*/
|
|
183
|
+
export const addBridgedEndpoint = async (pluginName, device) => {
|
|
184
|
+
try {
|
|
185
|
+
await aggregator.add(device);
|
|
186
|
+
return Promise.resolve(true);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
return Promise.reject(error);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Remove a bridged endpoint
|
|
194
|
+
*
|
|
195
|
+
* @param {string} pluginName The name of the plugin.
|
|
196
|
+
* @param {Endpoint} device The device to remove.
|
|
197
|
+
* @returns {Promise<boolean>} A promise that resolves to true if the endpoint was removed successfully, or rejects with an error if the operation failed.
|
|
198
|
+
*/
|
|
199
|
+
export const removeBridgedEndpoint = async (pluginName, device) => {
|
|
200
|
+
try {
|
|
201
|
+
await device.delete();
|
|
202
|
+
return Promise.resolve(true);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return Promise.reject(error);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Remove all bridged endpoints
|
|
210
|
+
*
|
|
211
|
+
* @param {string} _pluginName The name of the plugin.
|
|
212
|
+
* @param {number} _delay The delay before removing all endpoints.
|
|
213
|
+
* @returns {Promise<boolean>} A promise that resolves to true if all endpoints were removed successfully, or rejects with an error if the operation failed.
|
|
214
|
+
*/
|
|
215
|
+
export const removeAllBridgedEndpoints = async (_pluginName, _delay = 0) => {
|
|
216
|
+
try {
|
|
217
|
+
for (const device of aggregator.parts) {
|
|
218
|
+
await device.delete();
|
|
219
|
+
}
|
|
220
|
+
return Promise.resolve(true);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
return Promise.reject(error);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Add a virtual endpoint
|
|
228
|
+
*
|
|
229
|
+
* @param {string} pluginName The name of the plugin.
|
|
230
|
+
* @param {string} name The name of the virtual endpoint.
|
|
231
|
+
* @param {'light' | 'outlet' | 'switch' | 'mounted_switch'} type The type of the virtual endpoint.
|
|
232
|
+
* @param {() => Promise<void>} callback The callback to execute when the virtual endpoint is triggered.
|
|
233
|
+
* @returns {Promise<boolean>} A promise that resolves to true if the virtual endpoint was added successfully, or rejects with an error if the operation failed.
|
|
234
|
+
*/
|
|
235
|
+
export const addVirtualEndpoint = async (pluginName, name, type, callback) => {
|
|
236
|
+
try {
|
|
237
|
+
const device = new Endpoint(MountedOnOffControlDevice.with(BridgedDeviceBasicInformationServer), {
|
|
238
|
+
id: name.replaceAll(' ', '') + ':' + type,
|
|
239
|
+
bridgedDeviceBasicInformation: {
|
|
240
|
+
vendorId: VendorId(0xfff1),
|
|
241
|
+
vendorName: 'Matterbridge',
|
|
242
|
+
productName: 'Matterbridge Virtual Device',
|
|
243
|
+
nodeLabel: name.slice(0, 32),
|
|
244
|
+
softwareVersion: 20000,
|
|
245
|
+
softwareVersionString: '2.0.0',
|
|
246
|
+
},
|
|
247
|
+
onOff: { onOff: false },
|
|
248
|
+
});
|
|
249
|
+
// Set up an event listener for when the `onOff` state changes.
|
|
250
|
+
device.events.onOff.onOff$Changed.on((value) => {
|
|
251
|
+
// If the `onOff` state becomes true, turn off the virtual device and execute the callback.
|
|
252
|
+
if (value) {
|
|
253
|
+
void callback().catch(/* v8 ignore next */ noop);
|
|
254
|
+
void device.setStateOf(OnOffServer, { onOff: false }).catch(/* v8 ignore next */ noop);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
// Add the created device to the aggregator.
|
|
258
|
+
await aggregator.add(device);
|
|
259
|
+
// Add the OnOffPlugInUnit to MountedOnOffControlDevice (Matter 1.4.2 specs added this (new case of superset) for legacy controllers to recognize the mounted switch).
|
|
260
|
+
await device.act(async (agent) => {
|
|
261
|
+
const descriptor = await agent.load(DescriptorServer);
|
|
262
|
+
descriptor.addDeviceTypes('OnOffPlugInUnit');
|
|
263
|
+
});
|
|
264
|
+
// Initially set the state of the virtual device's `OnOffServer` to false (off).
|
|
265
|
+
await device.setStateOf(OnOffServer, { onOff: false });
|
|
266
|
+
return Promise.resolve(true);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
return Promise.reject(error);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* Inject matterbridge platform for testing.
|
|
274
|
+
*
|
|
275
|
+
* @param {object} platform The platform to inject.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```typescript
|
|
279
|
+
* platform = new MatterbridgePlatform(matterbridge, log, config);
|
|
280
|
+
* // Inject in the platform the Matterbridge environment
|
|
281
|
+
* addMatterbridge(platform);
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
export function addMatterbridge(platform) {
|
|
285
|
+
expect(platform).toBeDefined();
|
|
286
|
+
// Setup the platform MatterNode helpers
|
|
287
|
+
// @ts-expect-error - setMatterNode is intentionally private
|
|
288
|
+
platform.setMatterNode(addBridgedEndpoint, removeBridgedEndpoint, removeAllBridgedEndpoints, addVirtualEndpoint);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Flush (await) the lazy endpoint number persistence mechanism used by matter.js.
|
|
292
|
+
*
|
|
293
|
+
* Background:
|
|
294
|
+
* assignNumber() batches persistence (store.saveNumber + updating __nextNumber__) via an internal promise (#numbersPersisted).
|
|
295
|
+
* Calling endpointStores.close() waits for the current batch only. If new endpoints were added in the same macrotask
|
|
296
|
+
* cycle additional micro/macro turns might be needed to ensure the batch started. We defensively yield macrotasks
|
|
297
|
+
* (setImmediate) and then await close() multiple rounds.
|
|
298
|
+
*
|
|
299
|
+
* @param {ServerNode} targetServer The server whose endpoint numbering persistence should be flushed.
|
|
300
|
+
* @param {number} rounds Number of macrotask + close cycles to run (3 is usually sufficient).
|
|
301
|
+
* @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
|
|
302
|
+
* @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
|
|
303
|
+
* @param {number} pause Duration in ms to wait between cycles (default 10ms) to allow any follow-up work scheduled by close() to run.
|
|
304
|
+
* @returns {Promise<void>} Resolves when pending number persistence batches have completed.
|
|
305
|
+
*/
|
|
306
|
+
export async function flushAllEndpointNumberPersistence(targetServer, rounds = 3, ticks = 1, microTurns = 1, pause = 10) {
|
|
307
|
+
const nodeStore = targetServer.env.get(ServerNodeStore);
|
|
308
|
+
for (let i = 0; i < rounds; i++) {
|
|
309
|
+
await flushAsync(ticks, microTurns, pause);
|
|
310
|
+
await nodeStore.endpointStores.close();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Get the root ServerNode for a given Endpoint by traversing up the owner chain until the root endpoint is reached.
|
|
315
|
+
*
|
|
316
|
+
* @param {Endpoint} endpoint The endpoint to find the root server for.
|
|
317
|
+
* @returns {ServerNode<ServerNode.RootEndpoint>} The root ServerNode of the given endpoint.
|
|
318
|
+
*/
|
|
319
|
+
function getRootServerNode(endpoint) {
|
|
320
|
+
let current = endpoint;
|
|
321
|
+
while (current.owner) {
|
|
322
|
+
current = current.owner;
|
|
323
|
+
}
|
|
324
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion
|
|
325
|
+
return current;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Collect all endpoints in the server endpoint tree (root -> descendants).
|
|
329
|
+
*
|
|
330
|
+
* @param {Endpoint} root Root endpoint (typically the ServerNode root endpoint cast as Endpoint).
|
|
331
|
+
* @returns {Endpoint[]} Flat array including the root and every descendant once.
|
|
332
|
+
*/
|
|
333
|
+
function collectAllEndpoints(root) {
|
|
334
|
+
const list = [];
|
|
335
|
+
const walk = (ep) => {
|
|
336
|
+
list.push(ep);
|
|
337
|
+
for (const child of ep.parts) {
|
|
338
|
+
walk(child);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
walk(root);
|
|
342
|
+
return list;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Assert that every endpoint attached to the server has an assigned and (batch-)persisted endpoint number.
|
|
346
|
+
*
|
|
347
|
+
* This waits for any outstanding number persistence batch (endpointStores.close()), then traverses the endpoint
|
|
348
|
+
* graph and asserts:
|
|
349
|
+
* - Root endpoint: number is 0 (allowing undefined to coerce to 0 via nullish coalescing check).
|
|
350
|
+
* - All other endpoints: number > 0.
|
|
351
|
+
*
|
|
352
|
+
* @param {ServerNode} targetServer The server whose endpoint numbers are verified.
|
|
353
|
+
* @returns {Promise<void>} Resolves when assertions complete.
|
|
354
|
+
*/
|
|
355
|
+
export async function assertAllEndpointNumbersPersisted(targetServer) {
|
|
356
|
+
const nodeStore = targetServer.env.get(ServerNodeStore);
|
|
357
|
+
// Ensure any pending persistence finished (flush any in-flight batch promise)
|
|
358
|
+
await nodeStore.endpointStores.close();
|
|
359
|
+
const all = collectAllEndpoints(targetServer);
|
|
360
|
+
for (const ep of all) {
|
|
361
|
+
const store = nodeStore.storeForEndpoint(ep);
|
|
362
|
+
if (ep.maybeNumber === 0) {
|
|
363
|
+
expect(store.number ?? 0).toBe(0); // root
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
expect(store.number).toBeGreaterThan(0);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return all.length;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Close the server node stores to flush any pending endpoint number persistence.
|
|
373
|
+
*
|
|
374
|
+
* @param {ServerNode} targetServer The server whose endpoint stores should be closed.
|
|
375
|
+
* @returns {Promise<void>} Resolves when the stores have been closed.
|
|
376
|
+
*/
|
|
377
|
+
export async function closeServerNodeStores(targetServer) {
|
|
378
|
+
const resolvedTargetServer = targetServer ?? server;
|
|
379
|
+
await resolvedTargetServer?.env.get(ServerNodeStore)?.endpointStores.close();
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Create a matter server node for testing.
|
|
383
|
+
*
|
|
384
|
+
* @param {number} port TCP port to listen on.
|
|
385
|
+
* @param {DeviceTypeId} deviceType Device type identifier for the server node. Defaults to the bridge device type.
|
|
386
|
+
* @param {number} ticks Number of macrotask (setImmediate) turns to yield after starting the server to allow asynchronous work to complete before returning (default 1).
|
|
387
|
+
* @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
|
|
388
|
+
* @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server start process to run before returning (default 10ms).
|
|
389
|
+
* @returns {Promise<[ServerNode<ServerNode.RootEndpoint>, Endpoint<AggregatorEndpoint>]>} Resolves to an array containing the created ServerNode and its AggregatorNode.
|
|
390
|
+
* @example
|
|
391
|
+
* ```typescript
|
|
392
|
+
* const NAME = 'BaseTest';
|
|
393
|
+
* const MATTER_PORT = 8000;
|
|
394
|
+
* const MATTER_CREATE_ONLY = true;
|
|
395
|
+
*
|
|
396
|
+
* // Setup the test environment
|
|
397
|
+
* await setupTest(NAME, false);
|
|
398
|
+
*
|
|
399
|
+
* // Setup the Matter test environment
|
|
400
|
+
* await createTestEnvironment();
|
|
401
|
+
*
|
|
402
|
+
* // Create the server node and aggregator
|
|
403
|
+
* await createServerNode(MATTER_PORT);
|
|
404
|
+
*
|
|
405
|
+
* // Start the server node if not in create-only mode
|
|
406
|
+
* if (!MATTER_CREATE_ONLY) await startServerNode();
|
|
407
|
+
*
|
|
408
|
+
* // Stop or flush the server node depending on the create-only mode
|
|
409
|
+
* if (MATTER_CREATE_ONLY) await flushServerNode();
|
|
410
|
+
* else await stopServerNode();
|
|
411
|
+
*
|
|
412
|
+
* // Destroy the Matter test environment
|
|
413
|
+
* await destroyTestEnvironment();
|
|
414
|
+
* ```
|
|
415
|
+
*/
|
|
416
|
+
export async function createServerNode(port, deviceType = DeviceTypeId(0x000e), ticks = 1, microTurns = 1, pause = 10) {
|
|
417
|
+
const { randomBytes } = await import('node:crypto');
|
|
418
|
+
const random = randomBytes(8).toString('hex');
|
|
419
|
+
// Create the server node
|
|
420
|
+
server = await ServerNode.create({
|
|
421
|
+
id: NAME + 'ServerNode',
|
|
422
|
+
// Provide the environment
|
|
423
|
+
environment,
|
|
424
|
+
// Provide Node announcement settings
|
|
425
|
+
productDescription: {
|
|
426
|
+
name: NAME + 'ServerNode',
|
|
427
|
+
deviceType: DeviceTypeId(deviceType),
|
|
428
|
+
vendorId: VendorId(0xfff1),
|
|
429
|
+
productId: 0x8000,
|
|
430
|
+
},
|
|
431
|
+
// Provide defaults for the BasicInformation cluster on the Root endpoint
|
|
432
|
+
basicInformation: {
|
|
433
|
+
vendorId: VendorId(0xfff1),
|
|
434
|
+
vendorName: 'Matterbridge',
|
|
435
|
+
productId: 0x8000,
|
|
436
|
+
productName: ('Matterbridge ' + NAME).slice(0, 32), // truncate to 32 chars to satisfy BasicInformation cluster constraints
|
|
437
|
+
productLabel: ('Label ' + NAME).slice(0, 64), // truncate to 64 chars to satisfy BasicInformation cluster constraints
|
|
438
|
+
nodeLabel: (NAME + 'ServerNode').slice(0, 32), // truncate to 32 chars to satisfy BasicInformation cluster constraints
|
|
439
|
+
hardwareVersion: 1,
|
|
440
|
+
softwareVersion: 1,
|
|
441
|
+
productUrl: 'https://matterbridge.io',
|
|
442
|
+
configurationVersion: 1,
|
|
443
|
+
reachable: true,
|
|
444
|
+
serialNumber: 'SN' + random,
|
|
445
|
+
uniqueId: 'UI' + random,
|
|
446
|
+
},
|
|
447
|
+
// Provide Network relevant configuration like the port
|
|
448
|
+
network: {
|
|
449
|
+
listeningAddressIpv4: undefined,
|
|
450
|
+
listeningAddressIpv6: undefined,
|
|
451
|
+
port,
|
|
452
|
+
tcp: true,
|
|
453
|
+
transportPreference: 'udp',
|
|
454
|
+
},
|
|
455
|
+
// Provide the certificate for the device
|
|
456
|
+
operationalCredentials: {
|
|
457
|
+
certification: undefined,
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
expect(server).toBeDefined();
|
|
461
|
+
expect(server.lifecycle.isReady).toBeTruthy();
|
|
462
|
+
// Create the aggregator node
|
|
463
|
+
aggregator = new Endpoint(AggregatorEndpoint, {
|
|
464
|
+
id: NAME + 'AggregatorNode',
|
|
465
|
+
});
|
|
466
|
+
expect(aggregator).toBeDefined();
|
|
467
|
+
// Add the aggregator to the server
|
|
468
|
+
await server.add(aggregator);
|
|
469
|
+
expect(server.parts.has(aggregator.id)).toBeTruthy();
|
|
470
|
+
expect(server.parts.has(aggregator)).toBeTruthy();
|
|
471
|
+
expect(aggregator.lifecycle.isReady).toBeTruthy();
|
|
472
|
+
expect(server.lifecycle.isOnline).toBeFalsy();
|
|
473
|
+
// Ensure the queue is empty and pause 100ms to allow any pending work to complete before returning the server and aggregator
|
|
474
|
+
await flushAsync(ticks, microTurns, pause);
|
|
475
|
+
return [server, aggregator];
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Start a matter server node for testing.
|
|
479
|
+
*
|
|
480
|
+
* @param {number} ticks Number of macrotask (setImmediate) turns to yield after starting the server to allow asynchronous work to complete before returning (default 1).
|
|
481
|
+
* @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
|
|
482
|
+
* @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server start process to run before returning (default 10ms).
|
|
483
|
+
* @returns {Promise<[ServerNode<ServerNode.RootEndpoint>, Endpoint<AggregatorEndpoint>]>} Resolves to an array containing the created ServerNode and its AggregatorNode.
|
|
484
|
+
* @example
|
|
485
|
+
* ```typescript
|
|
486
|
+
* const NAME = 'BaseTest';
|
|
487
|
+
* const MATTER_PORT = 8000;
|
|
488
|
+
* const MATTER_CREATE_ONLY = true;
|
|
489
|
+
*
|
|
490
|
+
* // Setup the test environment
|
|
491
|
+
* await setupTest(NAME, false);
|
|
492
|
+
*
|
|
493
|
+
* // Setup the Matter test environment
|
|
494
|
+
* await createTestEnvironment();
|
|
495
|
+
*
|
|
496
|
+
* // Create the server node and aggregator
|
|
497
|
+
* await createServerNode(MATTER_PORT);
|
|
498
|
+
*
|
|
499
|
+
* // Start the server node if not in create-only mode
|
|
500
|
+
* if (!MATTER_CREATE_ONLY) await startServerNode();
|
|
501
|
+
*
|
|
502
|
+
* // Stop or flush the server node depending on the create-only mode
|
|
503
|
+
* if (MATTER_CREATE_ONLY) await flushServerNode();
|
|
504
|
+
* else await stopServerNode();
|
|
505
|
+
*
|
|
506
|
+
* // Destroy the Matter test environment
|
|
507
|
+
* await destroyTestEnvironment();
|
|
508
|
+
* ```
|
|
509
|
+
*/
|
|
510
|
+
export async function startServerNode(ticks = 1, microTurns = 1, pause = 10) {
|
|
511
|
+
// Create the server node
|
|
512
|
+
if (!server || !aggregator) {
|
|
513
|
+
/* v8 ignore next */
|
|
514
|
+
throw new Error('Server node and aggregator must be created before starting the server. Call createServerNode() first.');
|
|
515
|
+
}
|
|
516
|
+
// Wait for the server to be online
|
|
517
|
+
await new Promise((resolve, reject) => {
|
|
518
|
+
server.lifecycle.online.on(() => {
|
|
519
|
+
resolve();
|
|
520
|
+
});
|
|
521
|
+
server.start().catch((err) => reject(err));
|
|
522
|
+
});
|
|
523
|
+
// Check if the server is online
|
|
524
|
+
expect(server.lifecycle.isReady).toBeTruthy();
|
|
525
|
+
expect(server.lifecycle.isOnline).toBeTruthy();
|
|
526
|
+
expect(server.lifecycle.isCommissioned).toBeFalsy();
|
|
527
|
+
expect(server.lifecycle.isPartsReady).toBeTruthy();
|
|
528
|
+
expect(server.lifecycle.hasId).toBeTruthy();
|
|
529
|
+
expect(server.lifecycle.hasNumber).toBeTruthy();
|
|
530
|
+
expect(aggregator.lifecycle.isReady).toBeTruthy();
|
|
531
|
+
expect(aggregator.lifecycle.isInstalled).toBeTruthy();
|
|
532
|
+
expect(aggregator.lifecycle.isPartsReady).toBeTruthy();
|
|
533
|
+
expect(aggregator.lifecycle.hasId).toBeTruthy();
|
|
534
|
+
expect(aggregator.lifecycle.hasNumber).toBeTruthy();
|
|
535
|
+
// Ensure the queue is empty
|
|
536
|
+
await flushAsync(ticks, microTurns, pause);
|
|
537
|
+
return [server, aggregator];
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Stop a matter server node.
|
|
541
|
+
*
|
|
542
|
+
* @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
|
|
543
|
+
* @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
|
|
544
|
+
* @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server stop process to run before returning (default 10ms).
|
|
545
|
+
* @returns {Promise<void>} Resolves when the server has stopped.
|
|
546
|
+
* @example
|
|
547
|
+
* ```typescript
|
|
548
|
+
* const NAME = 'BaseTest';
|
|
549
|
+
* const MATTER_PORT = 8000;
|
|
550
|
+
* const MATTER_CREATE_ONLY = true;
|
|
551
|
+
*
|
|
552
|
+
* // Setup the test environment
|
|
553
|
+
* await setupTest(NAME, false);
|
|
554
|
+
*
|
|
555
|
+
* // Setup the Matter test environment
|
|
556
|
+
* await createTestEnvironment();
|
|
557
|
+
*
|
|
558
|
+
* // Create the server node and aggregator
|
|
559
|
+
* await createServerNode(MATTER_PORT);
|
|
560
|
+
*
|
|
561
|
+
* // Start the server node if not in create-only mode
|
|
562
|
+
* if (!MATTER_CREATE_ONLY) await startServerNode();
|
|
563
|
+
*
|
|
564
|
+
* // Stop or flush the server node depending on the create-only mode
|
|
565
|
+
* if (MATTER_CREATE_ONLY) await flushServerNode();
|
|
566
|
+
* else await stopServerNode();
|
|
567
|
+
*
|
|
568
|
+
* // Destroy the Matter test environment
|
|
569
|
+
* await destroyTestEnvironment();
|
|
570
|
+
* ```
|
|
571
|
+
*/
|
|
572
|
+
export async function stopServerNode(ticks = 1, microTurns = 1, pause = 10) {
|
|
573
|
+
// Flush any pending endpoint number persistence
|
|
574
|
+
await flushAllEndpointNumberPersistence(server);
|
|
575
|
+
// Ensure all endpoint numbers are persisted
|
|
576
|
+
await assertAllEndpointNumbersPersisted(server);
|
|
577
|
+
// Stop the server
|
|
578
|
+
expect(server).toBeDefined();
|
|
579
|
+
expect(server.lifecycle.isReady).toBeTruthy();
|
|
580
|
+
expect(server.lifecycle.isOnline).toBeTruthy();
|
|
581
|
+
// Close the server to release UDP sockets and other network resources created
|
|
582
|
+
// by ServerNode.create() (mDNS udp4/udp6 sockets), which would otherwise keep
|
|
583
|
+
// the Jest process alive indefinitely after all tests finish.
|
|
584
|
+
await server.close();
|
|
585
|
+
expect(server.lifecycle.isReady).toBeFalsy();
|
|
586
|
+
expect(server.lifecycle.isOnline).toBeFalsy();
|
|
587
|
+
// Ensure the queue is empty
|
|
588
|
+
await flushAsync(ticks, microTurns, pause);
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Flush any pending endpoint number persistence.
|
|
592
|
+
*
|
|
593
|
+
* @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
|
|
594
|
+
* @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
|
|
595
|
+
* @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server stop process to run before returning (default 10ms).
|
|
596
|
+
* @returns {Promise<void>} Resolves when the server has stopped.
|
|
597
|
+
* @example
|
|
598
|
+
* ```typescript
|
|
599
|
+
* const NAME = 'BaseTest';
|
|
600
|
+
* const MATTER_PORT = 8000;
|
|
601
|
+
* const MATTER_CREATE_ONLY = true;
|
|
602
|
+
*
|
|
603
|
+
* // Setup the test environment
|
|
604
|
+
* await setupTest(NAME, false);
|
|
605
|
+
*
|
|
606
|
+
* // Setup the Matter test environment
|
|
607
|
+
* await createTestEnvironment();
|
|
608
|
+
*
|
|
609
|
+
* // Create the server node and aggregator
|
|
610
|
+
* await createServerNode(MATTER_PORT);
|
|
611
|
+
*
|
|
612
|
+
* // Start the server node if not in create-only mode
|
|
613
|
+
* if (!MATTER_CREATE_ONLY) await startServerNode();
|
|
614
|
+
*
|
|
615
|
+
* // Stop or flush the server node depending on the create-only mode
|
|
616
|
+
* if (MATTER_CREATE_ONLY) await flushServerNode();
|
|
617
|
+
* else await stopServerNode();
|
|
618
|
+
*
|
|
619
|
+
* // Destroy the Matter test environment
|
|
620
|
+
* await destroyTestEnvironment();
|
|
621
|
+
* ```
|
|
622
|
+
*/
|
|
623
|
+
export async function flushServerNode(ticks = 1, microTurns = 1, pause = 10) {
|
|
624
|
+
// Flush any pending endpoint number persistence
|
|
625
|
+
await flushAllEndpointNumberPersistence(server);
|
|
626
|
+
// Ensure all endpoint numbers are persisted
|
|
627
|
+
await assertAllEndpointNumbersPersisted(server);
|
|
628
|
+
// Stop the server
|
|
629
|
+
expect(server).toBeDefined();
|
|
630
|
+
expect(server.lifecycle.isReady).toBeTruthy();
|
|
631
|
+
expect(server.lifecycle.isOnline).toBeFalsy();
|
|
632
|
+
// Close the server to release UDP sockets and other network resources created
|
|
633
|
+
// by ServerNode.create() (mDNS udp4/udp6 sockets), which would otherwise keep
|
|
634
|
+
// the Jest process alive indefinitely after all tests finish.
|
|
635
|
+
await server.close();
|
|
636
|
+
expect(server.lifecycle.isReady).toBeFalsy();
|
|
637
|
+
expect(server.lifecycle.isOnline).toBeFalsy();
|
|
638
|
+
// Ensure the queue is empty
|
|
639
|
+
await flushAsync(ticks, microTurns, pause);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Add a device (endpoint) to a matter server node or an aggregator.
|
|
643
|
+
*
|
|
644
|
+
* @param {ServerNode<ServerNode.RootEndpoint> | Endpoint<AggregatorEndpoint>} owner The server or aggregator to add the device to.
|
|
645
|
+
* @param {Endpoint} device The device to add.
|
|
646
|
+
* @param {number} rounds The number of rounds to perform after addition (default 3).
|
|
647
|
+
* @param {number} pause The pause time in milliseconds after addition (default 10ms).
|
|
648
|
+
* @returns {Promise<void>} Resolves when the device has been added and is ready.
|
|
649
|
+
* @example
|
|
650
|
+
* ```typescript
|
|
651
|
+
* expect(await addDevice(aggregator, device)).toBeTruthy();
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
export async function addDevice(owner, device, rounds = 3, pause = 10) {
|
|
655
|
+
expect(owner).toBeDefined();
|
|
656
|
+
expect(device).toBeDefined();
|
|
657
|
+
expect(owner.lifecycle.isReady).toBeTruthy();
|
|
658
|
+
expect(owner.construction.status).toBe(Lifecycle.Status.Active);
|
|
659
|
+
expect(owner.lifecycle.isPartsReady).toBeTruthy();
|
|
660
|
+
const rootServerNode = getRootServerNode(owner);
|
|
661
|
+
await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
|
|
662
|
+
/* v8 ignore next */
|
|
663
|
+
try {
|
|
664
|
+
await owner.add(device);
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
inspectError(log, `Error adding device ${device.maybeId}.${device.maybeNumber}`, error);
|
|
668
|
+
process.stderr.write(`${er}Error adding device ${device.maybeId}.${device.maybeNumber}${rs}\n`);
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
await device.construction.ready;
|
|
672
|
+
expect(owner.parts.has(device)).toBeTruthy();
|
|
673
|
+
expect(owner.lifecycle.isPartsReady).toBeTruthy();
|
|
674
|
+
expect(device.lifecycle.isReady).toBeTruthy();
|
|
675
|
+
expect(device.lifecycle.isInstalled).toBeTruthy();
|
|
676
|
+
expect(device.lifecycle.hasId).toBeTruthy();
|
|
677
|
+
expect(device.lifecycle.hasNumber).toBeTruthy();
|
|
678
|
+
expect(device.construction.status).toBe(Lifecycle.Status.Active);
|
|
679
|
+
await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Delete a device (endpoint) from a matter server node or an aggregator.
|
|
684
|
+
*
|
|
685
|
+
* @param {ServerNode<ServerNode.RootEndpoint> | Endpoint<AggregatorEndpoint>} owner The server or aggregator to remove the device from.
|
|
686
|
+
* @param {Endpoint} device The device to remove.
|
|
687
|
+
* @param {number} rounds The number of rounds to perform after deletion (default 3).
|
|
688
|
+
* @param {number} pause The pause time in milliseconds after deletion (default 10ms).
|
|
689
|
+
* @returns {Promise<void>} Resolves when the device has been removed and is no longer ready.
|
|
690
|
+
* @example
|
|
691
|
+
* ```typescript
|
|
692
|
+
* expect(await deleteDevice(aggregator, device)).toBeTruthy();
|
|
693
|
+
* ```
|
|
694
|
+
*/
|
|
695
|
+
export async function deleteDevice(owner, device, rounds = 3, pause = 10) {
|
|
696
|
+
expect(owner).toBeDefined();
|
|
697
|
+
expect(device).toBeDefined();
|
|
698
|
+
expect(owner.lifecycle.isReady).toBeTruthy();
|
|
699
|
+
expect(owner.construction.status).toBe(Lifecycle.Status.Active);
|
|
700
|
+
expect(owner.lifecycle.isPartsReady).toBeTruthy();
|
|
701
|
+
const rootServerNode = getRootServerNode(owner);
|
|
702
|
+
await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
|
|
703
|
+
/* v8 ignore next */
|
|
704
|
+
try {
|
|
705
|
+
await device.delete();
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
inspectError(log, `Error deleting device ${device.maybeId}.${device.maybeNumber}`, error);
|
|
709
|
+
process.stderr.write(`${er}Error deleting device ${device.maybeId}.${device.maybeNumber}${rs}\n`);
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
expect(owner.parts.has(device)).toBeFalsy();
|
|
713
|
+
expect(owner.lifecycle.isPartsReady).toBeTruthy();
|
|
714
|
+
expect(device.lifecycle.isReady).toBeFalsy();
|
|
715
|
+
expect(device.lifecycle.isInstalled).toBeFalsy();
|
|
716
|
+
expect(device.lifecycle.hasId).toBeTruthy();
|
|
717
|
+
expect(device.lifecycle.hasNumber).toBeTruthy();
|
|
718
|
+
expect(device.construction.status).toBe(Lifecycle.Status.Destroyed);
|
|
719
|
+
await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
//# sourceMappingURL=matterTest.js.map
|