@ya-modbus/mqtt-bridge 0.4.1-refactor-scope-driver-packages.0
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/CHANGELOG.md +41 -0
- package/LICENSE +674 -0
- package/README.md +190 -0
- package/dist/bin/ya-modbus-bridge.d.ts +9 -0
- package/dist/bin/ya-modbus-bridge.d.ts.map +1 -0
- package/dist/bin/ya-modbus-bridge.js +10 -0
- package/dist/bin/ya-modbus-bridge.js.map +1 -0
- package/dist/src/cli.d.ts +4 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +109 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/device-manager.d.ts +17 -0
- package/dist/src/device-manager.d.ts.map +1 -0
- package/dist/src/device-manager.js +79 -0
- package/dist/src/device-manager.js.map +1 -0
- package/dist/src/driver-loader.d.ts +53 -0
- package/dist/src/driver-loader.d.ts.map +1 -0
- package/dist/src/driver-loader.js +120 -0
- package/dist/src/driver-loader.js.map +1 -0
- package/dist/src/index.d.ts +13 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +285 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/polling-scheduler.d.ts +48 -0
- package/dist/src/polling-scheduler.d.ts.map +1 -0
- package/dist/src/polling-scheduler.js +128 -0
- package/dist/src/polling-scheduler.js.map +1 -0
- package/dist/src/types.d.ts +86 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/__mocks__/package-info.d.ts +9 -0
- package/dist/src/utils/__mocks__/package-info.d.ts.map +1 -0
- package/dist/src/utils/__mocks__/package-info.js +11 -0
- package/dist/src/utils/__mocks__/package-info.js.map +1 -0
- package/dist/src/utils/__mocks__/process.d.ts +20 -0
- package/dist/src/utils/__mocks__/process.d.ts.map +1 -0
- package/dist/src/utils/__mocks__/process.js +37 -0
- package/dist/src/utils/__mocks__/process.js.map +1 -0
- package/dist/src/utils/config-validator.d.ts +3 -0
- package/dist/src/utils/config-validator.d.ts.map +1 -0
- package/dist/src/utils/config-validator.js +32 -0
- package/dist/src/utils/config-validator.js.map +1 -0
- package/dist/src/utils/config.d.ts +3 -0
- package/dist/src/utils/config.d.ts.map +1 -0
- package/dist/src/utils/config.js +52 -0
- package/dist/src/utils/config.js.map +1 -0
- package/dist/src/utils/device-validation.d.ts +31 -0
- package/dist/src/utils/device-validation.d.ts.map +1 -0
- package/dist/src/utils/device-validation.js +70 -0
- package/dist/src/utils/device-validation.js.map +1 -0
- package/dist/src/utils/package-info.d.ts +5 -0
- package/dist/src/utils/package-info.d.ts.map +1 -0
- package/dist/src/utils/package-info.js +11 -0
- package/dist/src/utils/package-info.js.map +1 -0
- package/dist/src/utils/process.d.ts +10 -0
- package/dist/src/utils/process.d.ts.map +1 -0
- package/dist/src/utils/process.js +13 -0
- package/dist/src/utils/process.js.map +1 -0
- package/dist/src/utils/test-utils.d.ts +313 -0
- package/dist/src/utils/test-utils.d.ts.map +1 -0
- package/dist/src/utils/test-utils.js +535 -0
- package/dist/src/utils/test-utils.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-mock for process utilities
|
|
3
|
+
* Used in tests via jest.mock('./utils/process.js')
|
|
4
|
+
*/
|
|
5
|
+
import { jest, beforeEach } from '@jest/globals';
|
|
6
|
+
/**
|
|
7
|
+
* Map of registered signal handlers
|
|
8
|
+
* Populated by onSignal mock implementation
|
|
9
|
+
* Automatically cleared before each test
|
|
10
|
+
*/
|
|
11
|
+
export const signalHandlers = new Map();
|
|
12
|
+
/**
|
|
13
|
+
* Trigger a signal handler that was registered via onSignal
|
|
14
|
+
*
|
|
15
|
+
* @param signal - The signal to trigger (e.g., 'SIGINT', 'SIGTERM')
|
|
16
|
+
* @returns true if handler was found and called, false otherwise
|
|
17
|
+
*/
|
|
18
|
+
export function triggerSignal(signal) {
|
|
19
|
+
const handler = signalHandlers.get(signal);
|
|
20
|
+
if (handler) {
|
|
21
|
+
handler();
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
// Automatically reset signal handlers before each test
|
|
27
|
+
// This ensures test isolation without requiring explicit cleanup
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
signalHandlers.clear();
|
|
30
|
+
});
|
|
31
|
+
export const processUtils = {
|
|
32
|
+
exit: jest.fn(),
|
|
33
|
+
onSignal: jest.fn((signal, handler) => {
|
|
34
|
+
signalHandlers.set(signal, handler);
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
//# sourceMappingURL=process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.js","sourceRoot":"","sources":["../../../../src/utils/__mocks__/process.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAIhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAsB,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC1C,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,EAAE,CAAA;QACT,OAAO,IAAI,CAAA;IACb,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,uDAAuD;AACvD,iEAAiE;AACjE,UAAU,CAAC,GAAG,EAAE;IACd,cAAc,CAAC,KAAK,EAAE,CAAA;AACxB,CAAC,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;IACf,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,MAAsB,EAAE,OAAmB,EAAE,EAAE;QAChE,cAAc,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,CAAC,CAAC;CACH,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-validator.d.ts","sourceRoot":"","sources":["../../../src/utils/config-validator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AA4BnD,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,gBAAgB,CAOlF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const mqttConfigSchema = z.object({
|
|
3
|
+
url: z
|
|
4
|
+
.string()
|
|
5
|
+
.url()
|
|
6
|
+
.regex(/^(mqtt|mqtts|ws|wss):\/\//, {
|
|
7
|
+
message: 'URL must start with mqtt://, mqtts://, ws://, or wss://',
|
|
8
|
+
}),
|
|
9
|
+
clientId: z.string().optional(),
|
|
10
|
+
username: z.string().optional(),
|
|
11
|
+
password: z.string().optional(),
|
|
12
|
+
reconnectPeriod: z.number().int().positive().optional(),
|
|
13
|
+
});
|
|
14
|
+
const mqttBridgeConfigSchema = z.object({
|
|
15
|
+
mqtt: mqttConfigSchema,
|
|
16
|
+
stateDir: z.string().optional(),
|
|
17
|
+
topicPrefix: z
|
|
18
|
+
.string()
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
.regex(/^[^+#/$\x00]+$/, {
|
|
21
|
+
message: 'Topic prefix must not contain MQTT special characters (+, #, /, $) or null character',
|
|
22
|
+
})
|
|
23
|
+
.optional(),
|
|
24
|
+
});
|
|
25
|
+
export function validateConfig(config) {
|
|
26
|
+
const result = mqttBridgeConfigSchema.safeParse(config);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
const errors = result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
|
29
|
+
throw new Error(`Invalid configuration: ${errors}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=config-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-validator.js","sourceRoot":"","sources":["../../../src/utils/config-validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAIvB,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,GAAG,EAAE,CAAC;SACH,MAAM,EAAE;SACR,GAAG,EAAE;SACL,KAAK,CAAC,2BAA2B,EAAE;QAClC,OAAO,EAAE,yDAAyD;KACnE,CAAC;IACJ,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;CACxD,CAAC,CAAA;AAEF,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,IAAI,EAAE,gBAAgB;IACtB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;QACT,4CAA4C;SAC3C,KAAK,CAAC,gBAAgB,EAAE;QACvB,OAAO,EACL,sFAAsF;KACzF,CAAC;SACD,QAAQ,EAAE;CACd,CAAC,CAAA;AAEF,MAAM,UAAU,cAAc,CAAC,MAAe;IAC5C,MAAM,MAAM,GAAG,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;IAEvD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC7F,MAAM,IAAI,KAAK,CAAC,0BAA0B,MAAM,EAAE,CAAC,CAAA;IACrD,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/utils/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AA6BnD,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA4B9E"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const mqttConfigSchema = z.object({
|
|
4
|
+
url: z
|
|
5
|
+
.string()
|
|
6
|
+
.url()
|
|
7
|
+
.regex(/^(mqtt|mqtts|ws|wss):\/\//, {
|
|
8
|
+
message: 'URL must start with mqtt://, mqtts://, ws://, or wss://',
|
|
9
|
+
})
|
|
10
|
+
.default('mqtt://localhost:1883'),
|
|
11
|
+
clientId: z.string().optional(),
|
|
12
|
+
username: z.string().optional(),
|
|
13
|
+
password: z.string().optional(),
|
|
14
|
+
reconnectPeriod: z.number().int().positive().optional(),
|
|
15
|
+
});
|
|
16
|
+
const mqttBridgeConfigSchema = z.object({
|
|
17
|
+
mqtt: mqttConfigSchema.default({ url: 'mqtt://localhost:1883' }),
|
|
18
|
+
stateDir: z.string().optional(),
|
|
19
|
+
topicPrefix: z
|
|
20
|
+
.string()
|
|
21
|
+
// eslint-disable-next-line no-control-regex
|
|
22
|
+
.regex(/^[^+#/$\x00]+$/, {
|
|
23
|
+
message: 'Topic prefix must not contain MQTT special characters (+, #, /, $) or null character',
|
|
24
|
+
})
|
|
25
|
+
.optional(),
|
|
26
|
+
});
|
|
27
|
+
export async function loadConfig(configPath) {
|
|
28
|
+
const content = await readFile(configPath, 'utf-8');
|
|
29
|
+
const json = JSON.parse(content);
|
|
30
|
+
/* istanbul ignore next - defensive: JSON.parse never returns null */
|
|
31
|
+
const result = mqttBridgeConfigSchema.safeParse(json === null ? {} : json);
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
const errors = result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
|
34
|
+
throw new Error(`Invalid configuration: ${errors}`);
|
|
35
|
+
}
|
|
36
|
+
// Convert Zod output (with T | undefined) to MqttBridgeConfig (with T?)
|
|
37
|
+
const config = {
|
|
38
|
+
mqtt: {
|
|
39
|
+
url: result.data.mqtt.url,
|
|
40
|
+
...(result.data.mqtt.clientId !== undefined && { clientId: result.data.mqtt.clientId }),
|
|
41
|
+
...(result.data.mqtt.username !== undefined && { username: result.data.mqtt.username }),
|
|
42
|
+
...(result.data.mqtt.password !== undefined && { password: result.data.mqtt.password }),
|
|
43
|
+
...(result.data.mqtt.reconnectPeriod !== undefined && {
|
|
44
|
+
reconnectPeriod: result.data.mqtt.reconnectPeriod,
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
...(result.data.stateDir !== undefined && { stateDir: result.data.stateDir }),
|
|
48
|
+
...(result.data.topicPrefix !== undefined && { topicPrefix: result.data.topicPrefix }),
|
|
49
|
+
};
|
|
50
|
+
return config;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/utils/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const deviceConfigSchema: z.ZodObject<{
|
|
3
|
+
deviceId: z.ZodString;
|
|
4
|
+
driver: z.ZodString;
|
|
5
|
+
connection: z.ZodUnion<readonly [z.ZodObject<{
|
|
6
|
+
type: z.ZodLiteral<"rtu">;
|
|
7
|
+
port: z.ZodString;
|
|
8
|
+
baudRate: z.ZodNumber;
|
|
9
|
+
slaveId: z.ZodNumber;
|
|
10
|
+
parity: z.ZodOptional<z.ZodEnum<{
|
|
11
|
+
none: "none";
|
|
12
|
+
even: "even";
|
|
13
|
+
odd: "odd";
|
|
14
|
+
}>>;
|
|
15
|
+
dataBits: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<7>, z.ZodLiteral<8>]>>;
|
|
16
|
+
stopBits: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>]>>;
|
|
17
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
18
|
+
type: z.ZodLiteral<"tcp">;
|
|
19
|
+
host: z.ZodString;
|
|
20
|
+
port: z.ZodNumber;
|
|
21
|
+
slaveId: z.ZodNumber;
|
|
22
|
+
}, z.core.$strip>]>;
|
|
23
|
+
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
24
|
+
polling: z.ZodOptional<z.ZodObject<{
|
|
25
|
+
interval: z.ZodNumber;
|
|
26
|
+
maxRetries: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
retryBackoff: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
export declare function validateDeviceConfig(config: unknown): void;
|
|
31
|
+
//# sourceMappingURL=device-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-validation.d.ts","sourceRoot":"","sources":["../../../src/utils/device-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA+DvB,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAM7B,CAAA;AAEF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAO1D"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// MQTT topic segment requirements:
|
|
3
|
+
// - Cannot contain null character
|
|
4
|
+
// - Cannot contain + or # (MQTT wildcards)
|
|
5
|
+
// - Cannot start with $ (reserved for system topics)
|
|
6
|
+
// - Cannot contain / (topic level separator)
|
|
7
|
+
// eslint-disable-next-line no-control-regex
|
|
8
|
+
const mqttTopicSegmentRegex = /^[^+#$/\u0000]+$/;
|
|
9
|
+
const deviceIdSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1, 'Device ID must not be empty')
|
|
12
|
+
.regex(mqttTopicSegmentRegex, {
|
|
13
|
+
message: 'Device ID must not contain MQTT special characters (+, #, /, $) or null character',
|
|
14
|
+
});
|
|
15
|
+
const driverNameSchema = z
|
|
16
|
+
.string()
|
|
17
|
+
.min(1, 'Driver name must not be empty')
|
|
18
|
+
.regex(/^(@ya-modbus\/driver-[a-z0-9-]+|ya-modbus-driver-[a-z0-9-]+)$/, {
|
|
19
|
+
message: 'Driver must be @ya-modbus/driver-<name> or ya-modbus-driver-<name> with only lowercase letters, numbers, and hyphens',
|
|
20
|
+
})
|
|
21
|
+
.refine((name) => !name.includes('..') && !name.includes('\\'), {
|
|
22
|
+
message: 'Driver name cannot contain path traversal sequences',
|
|
23
|
+
});
|
|
24
|
+
const rtuConnectionSchema = z.object({
|
|
25
|
+
type: z.literal('rtu'),
|
|
26
|
+
port: z.string().min(1),
|
|
27
|
+
baudRate: z.number().positive(),
|
|
28
|
+
slaveId: z.number().int().min(0).max(247),
|
|
29
|
+
parity: z.enum(['none', 'even', 'odd']).optional(),
|
|
30
|
+
dataBits: z.union([z.literal(7), z.literal(8)]).optional(),
|
|
31
|
+
stopBits: z.union([z.literal(1), z.literal(2)]).optional(),
|
|
32
|
+
});
|
|
33
|
+
const tcpConnectionSchema = z.object({
|
|
34
|
+
type: z.literal('tcp'),
|
|
35
|
+
host: z.string().min(1),
|
|
36
|
+
port: z.number().int().positive().max(65535),
|
|
37
|
+
slaveId: z.number().int().min(0).max(247),
|
|
38
|
+
});
|
|
39
|
+
const pollingConfigSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
interval: z
|
|
42
|
+
.number()
|
|
43
|
+
.int()
|
|
44
|
+
.positive()
|
|
45
|
+
.min(100, 'Polling interval must be at least 100ms')
|
|
46
|
+
.max(86400000, 'Polling interval must not exceed 24 hours'),
|
|
47
|
+
maxRetries: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.nonnegative()
|
|
51
|
+
.max(100, 'Max retries should not exceed 100')
|
|
52
|
+
.optional(),
|
|
53
|
+
retryBackoff: z.number().int().positive().optional(),
|
|
54
|
+
})
|
|
55
|
+
.optional();
|
|
56
|
+
export const deviceConfigSchema = z.object({
|
|
57
|
+
deviceId: deviceIdSchema,
|
|
58
|
+
driver: driverNameSchema,
|
|
59
|
+
connection: z.union([rtuConnectionSchema, tcpConnectionSchema]),
|
|
60
|
+
enabled: z.boolean().optional(),
|
|
61
|
+
polling: pollingConfigSchema,
|
|
62
|
+
});
|
|
63
|
+
export function validateDeviceConfig(config) {
|
|
64
|
+
const result = deviceConfigSchema.safeParse(config);
|
|
65
|
+
if (!result.success) {
|
|
66
|
+
const errors = result.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ');
|
|
67
|
+
throw new Error(`Invalid device configuration: ${errors}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=device-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-validation.js","sourceRoot":"","sources":["../../../src/utils/device-validation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-info.d.ts","sourceRoot":"","sources":["../../../src/utils/package-info.ts"],"names":[],"mappings":"AAIA,wBAAgB,cAAc,IAAI;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CASzE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export function getPackageInfo() {
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
// When compiled, this file is at dist/src/utils/package-info.js
|
|
7
|
+
// So we need to go up 3 levels to reach package.json
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../../package.json'), 'utf-8'));
|
|
9
|
+
return pkg;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=package-info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-info.js","sourceRoot":"","sources":["../../../src/utils/package-info.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,MAAM,UAAU,cAAc;IAC5B,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACzD,gEAAgE;IAChE,qDAAqD;IACrD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,uBAAuB,CAAC,EAAE,OAAO,CAAC,CAGrF,CAAA;IACD,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process utilities for signal handling and exit
|
|
3
|
+
* Extracted to separate module for testability
|
|
4
|
+
*/
|
|
5
|
+
export interface ProcessUtils {
|
|
6
|
+
exit(code: number): void;
|
|
7
|
+
onSignal(signal: NodeJS.Signals, handler: () => void): void;
|
|
8
|
+
}
|
|
9
|
+
export declare const processUtils: ProcessUtils;
|
|
10
|
+
//# sourceMappingURL=process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.d.ts","sourceRoot":"","sources":["../../../src/utils/process.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAA;CAC5D;AAED,eAAO,MAAM,YAAY,EAAE,YAQ1B,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process utilities for signal handling and exit
|
|
3
|
+
* Extracted to separate module for testability
|
|
4
|
+
*/
|
|
5
|
+
export const processUtils = {
|
|
6
|
+
exit(code) {
|
|
7
|
+
process.exit(code);
|
|
8
|
+
},
|
|
9
|
+
onSignal(signal, handler) {
|
|
10
|
+
process.on(signal, handler);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process.js","sourceRoot":"","sources":["../../../src/utils/process.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,MAAM,CAAC,MAAM,YAAY,GAAiB;IACxC,IAAI,CAAC,IAAY;QACf,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpB,CAAC;IAED,QAAQ,CAAC,MAAsB,EAAE,OAAmB;QAClD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,CAAC;CACF,CAAA"}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { AddressInfo, Server } from 'node:net';
|
|
2
|
+
import { jest } from '@jest/globals';
|
|
3
|
+
import type { Transport, DeviceDriver } from '@ya-modbus/driver-types';
|
|
4
|
+
import { TransportManager } from '@ya-modbus/transport';
|
|
5
|
+
import Aedes from 'aedes';
|
|
6
|
+
import { DriverLoader } from '../driver-loader.js';
|
|
7
|
+
import type { MessageHandler, MqttBridge, MqttBridgeConfig, PublishOptions, SubscribeOptions } from '../types.js';
|
|
8
|
+
export interface TestBroker {
|
|
9
|
+
address: AddressInfo;
|
|
10
|
+
url: string;
|
|
11
|
+
port: number;
|
|
12
|
+
broker: Aedes;
|
|
13
|
+
server: Server;
|
|
14
|
+
close: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Race a promise against a timeout, properly cleaning up the timer
|
|
18
|
+
*
|
|
19
|
+
* This ensures Jest can exit cleanly by clearing the timeout when the promise settles.
|
|
20
|
+
* Always use this instead of bare Promise.race with setTimeout.
|
|
21
|
+
*
|
|
22
|
+
* @param promise - The promise to race
|
|
23
|
+
* @param timeoutMs - Timeout in milliseconds
|
|
24
|
+
* @param errorMessage - Error message if timeout occurs, or a function that returns the message
|
|
25
|
+
* @returns Promise that resolves/rejects with the first settled result
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // With static message
|
|
29
|
+
* await withTimeout(
|
|
30
|
+
* clientReadyPromise,
|
|
31
|
+
* 5000,
|
|
32
|
+
* 'Client ready timeout'
|
|
33
|
+
* )
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // With dynamic message (evaluated when timeout occurs)
|
|
37
|
+
* await withTimeout(
|
|
38
|
+
* disconnectPromise,
|
|
39
|
+
* 5000,
|
|
40
|
+
* () => `Still connected: ${broker.connectedClients}`
|
|
41
|
+
* )
|
|
42
|
+
*/
|
|
43
|
+
export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string | (() => string)): Promise<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Wait for all MQTT clients to disconnect from a test broker
|
|
46
|
+
*
|
|
47
|
+
* @param broker - The test broker to monitor
|
|
48
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 5000)
|
|
49
|
+
* @returns Promise that resolves when all clients disconnect or rejects on timeout
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* await waitForAllClientsToDisconnect(broker, 5000)
|
|
53
|
+
*/
|
|
54
|
+
export declare function waitForAllClientsToDisconnect(broker: TestBroker, timeoutMs?: number): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Wait for a client to be ready
|
|
57
|
+
*
|
|
58
|
+
* @param broker - The test broker to monitor
|
|
59
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 2000)
|
|
60
|
+
* @returns Promise that resolves when a client is ready or rejects on timeout
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* await waitForClientReady(broker)
|
|
64
|
+
*/
|
|
65
|
+
export declare function waitForClientReady(broker: TestBroker, timeoutMs?: number): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Wait for a client to disconnect
|
|
68
|
+
*
|
|
69
|
+
* @param broker - The test broker to monitor
|
|
70
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 2000)
|
|
71
|
+
* @returns Promise that resolves when a client disconnects or rejects on timeout
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* await waitForClientDisconnect(broker)
|
|
75
|
+
*/
|
|
76
|
+
export declare function waitForClientDisconnect(broker: TestBroker, timeoutMs?: number): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Wait for a publish event on the broker
|
|
79
|
+
*
|
|
80
|
+
* @param broker - The test broker to monitor
|
|
81
|
+
* @param topicPattern - Optional topic to match (supports wildcards)
|
|
82
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 1000)
|
|
83
|
+
* @returns Promise that resolves with the published packet or rejects on timeout
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* const packet = await waitForPublish(broker, 'test/topic')
|
|
87
|
+
*/
|
|
88
|
+
export declare function waitForPublish(broker: TestBroker, topicPattern?: string, timeoutMs?: number): Promise<{
|
|
89
|
+
topic: string;
|
|
90
|
+
payload: Buffer;
|
|
91
|
+
}>;
|
|
92
|
+
/**
|
|
93
|
+
* Wait for a subscribe event on the broker
|
|
94
|
+
*
|
|
95
|
+
* @param broker - The test broker to monitor
|
|
96
|
+
* @param topicPattern - Optional topic to match
|
|
97
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 1000)
|
|
98
|
+
* @returns Promise that resolves with subscriptions or rejects on timeout
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* await waitForSubscribe(broker, 'test/topic')
|
|
102
|
+
*/
|
|
103
|
+
export declare function waitForSubscribe(broker: TestBroker, topicPattern?: string, timeoutMs?: number): Promise<Array<{
|
|
104
|
+
topic: string;
|
|
105
|
+
}>>;
|
|
106
|
+
/**
|
|
107
|
+
* Wait for an unsubscribe event on the broker
|
|
108
|
+
*
|
|
109
|
+
* @param broker - The test broker to monitor
|
|
110
|
+
* @param topicPattern - Optional topic to match
|
|
111
|
+
* @param timeoutMs - Maximum time to wait in milliseconds (default: 1000)
|
|
112
|
+
* @returns Promise that resolves with unsubscriptions or rejects on timeout
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* await waitForUnsubscribe(broker, 'test/topic')
|
|
116
|
+
*/
|
|
117
|
+
export declare function waitForUnsubscribe(broker: TestBroker, topicPattern?: string, timeoutMs?: number): Promise<Array<string>>;
|
|
118
|
+
/**
|
|
119
|
+
* Create a test bridge configuration with optional overrides
|
|
120
|
+
*
|
|
121
|
+
* @param broker - The test broker to connect to
|
|
122
|
+
* @param overrides - Optional configuration overrides
|
|
123
|
+
* @returns Bridge configuration for testing
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* const config = createTestBridgeConfig(broker, { topicPrefix: 'custom' })
|
|
127
|
+
*/
|
|
128
|
+
export declare function createTestBridgeConfig(broker: TestBroker, overrides?: Partial<MqttBridgeConfig>): MqttBridgeConfig;
|
|
129
|
+
/**
|
|
130
|
+
* Subscribe to a topic and wait for the subscription to be registered with the broker
|
|
131
|
+
*
|
|
132
|
+
* @param bridge - The MQTT bridge instance
|
|
133
|
+
* @param broker - The test broker to monitor
|
|
134
|
+
* @param topic - Topic to subscribe to (without prefix)
|
|
135
|
+
* @param handler - Message handler function
|
|
136
|
+
* @param options - Subscribe options and topic prefix
|
|
137
|
+
* @returns Promise that resolves when subscription is registered
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* await subscribeAndWait(bridge, broker, 'test/topic', (msg) => { ... })
|
|
141
|
+
*/
|
|
142
|
+
export declare function subscribeAndWait(bridge: MqttBridge, broker: TestBroker, topic: string, handler: MessageHandler, options?: SubscribeOptions & {
|
|
143
|
+
prefix?: string;
|
|
144
|
+
}): Promise<void>;
|
|
145
|
+
/**
|
|
146
|
+
* Publish a message and wait for it to be published to the broker
|
|
147
|
+
*
|
|
148
|
+
* @param bridge - The MQTT bridge instance
|
|
149
|
+
* @param broker - The test broker to monitor
|
|
150
|
+
* @param topic - Topic to publish to (without prefix)
|
|
151
|
+
* @param message - Message payload
|
|
152
|
+
* @param options - Publish options and topic prefix
|
|
153
|
+
* @returns Promise that resolves when message is published
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* await publishAndWait(bridge, broker, 'test/topic', 'Hello', { qos: 1 })
|
|
157
|
+
*/
|
|
158
|
+
export declare function publishAndWait(bridge: MqttBridge, broker: TestBroker, topic: string, message: string | Buffer, options?: PublishOptions & {
|
|
159
|
+
prefix?: string;
|
|
160
|
+
}): Promise<void>;
|
|
161
|
+
/**
|
|
162
|
+
* Message collector for capturing messages in tests
|
|
163
|
+
*/
|
|
164
|
+
export interface MessageCollector {
|
|
165
|
+
messages: string[];
|
|
166
|
+
handler: MessageHandler;
|
|
167
|
+
clear: () => void;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a message collector for capturing messages in tests
|
|
171
|
+
*
|
|
172
|
+
* @returns Message collector with handler and utilities
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const collector = createMessageCollector()
|
|
176
|
+
* await bridge.subscribe('test/topic', collector.handler)
|
|
177
|
+
* expect(collector.messages).toContain('Expected message')
|
|
178
|
+
*/
|
|
179
|
+
export declare function createMessageCollector(): MessageCollector;
|
|
180
|
+
/**
|
|
181
|
+
* Execute a test function with a running bridge, ensuring proper cleanup
|
|
182
|
+
*
|
|
183
|
+
* Automatically starts the bridge before the test and stops it after,
|
|
184
|
+
* even if the test throws an error. This eliminates boilerplate and
|
|
185
|
+
* ensures resources are properly cleaned up.
|
|
186
|
+
*
|
|
187
|
+
* @param config - Bridge configuration
|
|
188
|
+
* @param testFn - Test function to execute with the running bridge (can be sync or async)
|
|
189
|
+
* @returns Promise that resolves when test completes and bridge is stopped
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* await withBridge(createTestBridgeConfig(broker), async (bridge) => {
|
|
193
|
+
* const status = bridge.getStatus()
|
|
194
|
+
* expect(status.state).toBe('running')
|
|
195
|
+
* })
|
|
196
|
+
*/
|
|
197
|
+
export declare function withBridge(config: MqttBridgeConfig, testFn: (bridge: MqttBridge) => Promise<void> | void): Promise<void>;
|
|
198
|
+
/**
|
|
199
|
+
* Execute a test function with a running bridge using mock driver DI
|
|
200
|
+
*
|
|
201
|
+
* Automatically sets up the bridge with injected mocks, starts it before the test,
|
|
202
|
+
* and stops it after. This eliminates boilerplate for tests that need to verify
|
|
203
|
+
* driver lifecycle with dependency injection.
|
|
204
|
+
*
|
|
205
|
+
* @param broker - The test broker to connect to
|
|
206
|
+
* @param testFn - Test function with bridge and mocks
|
|
207
|
+
* @param overrides - Optional configuration overrides
|
|
208
|
+
* @returns Promise that resolves when test completes and bridge is stopped
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* await withBridgeAndMockDriver(broker, async (bridge, mocks) => {
|
|
212
|
+
* await bridge.addDevice({ deviceId: 'test', driver: 'mock', connection: {...} })
|
|
213
|
+
* expect(mocks.mockDriver.initialize).toHaveBeenCalled()
|
|
214
|
+
* })
|
|
215
|
+
*/
|
|
216
|
+
export declare function withBridgeAndMockDriver(broker: TestBroker, testFn: (bridge: MqttBridge, mocks: {
|
|
217
|
+
mockDriver: jest.Mocked<DeviceDriver>;
|
|
218
|
+
mockTransport: jest.Mocked<Transport>;
|
|
219
|
+
mockLoadDriverFn: jest.Mock;
|
|
220
|
+
mockTransportManager: jest.Mocked<TransportManager>;
|
|
221
|
+
}) => Promise<void> | void, overrides?: Partial<MqttBridgeConfig>): Promise<void>;
|
|
222
|
+
/**
|
|
223
|
+
* Start an Aedes MQTT broker on a dynamic port for testing
|
|
224
|
+
*/
|
|
225
|
+
export declare function startTestBroker(options?: {
|
|
226
|
+
port?: number;
|
|
227
|
+
}): Promise<TestBroker>;
|
|
228
|
+
/**
|
|
229
|
+
* Create a mock transport for testing
|
|
230
|
+
*
|
|
231
|
+
* Returns a transport mock that implements the Transport interface
|
|
232
|
+
* with jest.fn() for all methods, allowing verification of calls.
|
|
233
|
+
*
|
|
234
|
+
* @returns Mock transport with trackable method calls
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* const mockTransport = createMockTransport()
|
|
238
|
+
* mockTransport.readHoldingRegisters.mockResolvedValue([1, 2, 3])
|
|
239
|
+
*/
|
|
240
|
+
export declare function createMockTransport(): jest.Mocked<Transport>;
|
|
241
|
+
/**
|
|
242
|
+
* Create a mock TransportManager for testing
|
|
243
|
+
*
|
|
244
|
+
* Returns a TransportManager mock that implements the TransportManager interface
|
|
245
|
+
* with jest.fn() for all methods, allowing verification of calls.
|
|
246
|
+
*
|
|
247
|
+
* @returns Mock TransportManager with trackable method calls
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* const mockTransportManager = createMockTransportManager()
|
|
251
|
+
* const loader = new DriverLoader(mockLoadDriver, mockTransportManager)
|
|
252
|
+
*/
|
|
253
|
+
export declare function createMockTransportManager(): jest.Mocked<TransportManager>;
|
|
254
|
+
/**
|
|
255
|
+
* Create a mock driver for testing
|
|
256
|
+
*
|
|
257
|
+
* Returns a mock driver that implements the DeviceDriver interface
|
|
258
|
+
* with jest.fn() for all methods, allowing verification of calls.
|
|
259
|
+
*
|
|
260
|
+
* @param overrides - Optional property overrides
|
|
261
|
+
* @returns Mock driver with trackable method calls
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* const mockDriver = createMockDriver({
|
|
265
|
+
* dataPoints: [{ id: 'voltage', name: 'Voltage', type: 'number', unit: 'V' }]
|
|
266
|
+
* })
|
|
267
|
+
* mockDriver.readDataPoints.mockResolvedValue({ voltage: 230 })
|
|
268
|
+
*/
|
|
269
|
+
export declare function createMockDriver(overrides?: {
|
|
270
|
+
name?: string;
|
|
271
|
+
manufacturer?: string;
|
|
272
|
+
model?: string;
|
|
273
|
+
dataPoints?: Array<{
|
|
274
|
+
id: string;
|
|
275
|
+
name: string;
|
|
276
|
+
type: string;
|
|
277
|
+
unit?: string;
|
|
278
|
+
}>;
|
|
279
|
+
}): jest.Mocked<DeviceDriver>;
|
|
280
|
+
/**
|
|
281
|
+
* Create a test bridge configuration with mock driver injection
|
|
282
|
+
*
|
|
283
|
+
* This configures the bridge with dependency injection for testing,
|
|
284
|
+
* allowing integration tests to verify driver lifecycle without
|
|
285
|
+
* loading real driver packages.
|
|
286
|
+
*
|
|
287
|
+
* **Note**: Returns fresh mock instances on each call. Mock state does
|
|
288
|
+
* not persist between invocations. Each test gets independent mocks.
|
|
289
|
+
*
|
|
290
|
+
* @param broker - The test broker to connect to
|
|
291
|
+
* @param overrides - Optional configuration overrides
|
|
292
|
+
* @returns Object with bridge config and driver loader
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* const { config, driverLoader, mockDriver } = createTestBridgeWithMockDriver(broker)
|
|
296
|
+
* const bridge = createBridge(config, { driverLoader })
|
|
297
|
+
* await bridge.addDevice({
|
|
298
|
+
* deviceId: 'test-device',
|
|
299
|
+
* driver: 'ya-modbus-driver-test',
|
|
300
|
+
* connection: { type: 'tcp', host: 'localhost', port: 502, slaveId: 1 },
|
|
301
|
+
* })
|
|
302
|
+
* // Verify driver lifecycle with injected mocks
|
|
303
|
+
* expect(mockDriver.initialize).toHaveBeenCalled()
|
|
304
|
+
*/
|
|
305
|
+
export declare function createTestBridgeWithMockDriver(broker: TestBroker, overrides?: Partial<MqttBridgeConfig>): {
|
|
306
|
+
config: MqttBridgeConfig;
|
|
307
|
+
driverLoader: DriverLoader;
|
|
308
|
+
mockDriver: jest.Mocked<DeviceDriver>;
|
|
309
|
+
mockTransport: jest.Mocked<Transport>;
|
|
310
|
+
mockLoadDriverFn: jest.Mock;
|
|
311
|
+
mockTransportManager: jest.Mocked<TransportManager>;
|
|
312
|
+
};
|
|
313
|
+
//# sourceMappingURL=test-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../../../src/utils/test-utils.ts"],"names":[],"mappings":"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"}
|