homebridge-securitysystem 0.0.1-matter
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/.gitattributes +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +46 -0
- package/.github/ISSUE_TEMPLATE/feature-request.yml +17 -0
- package/.github/ISSUE_TEMPLATE/questions-answers.yml +40 -0
- package/.github/demo.jpg +0 -0
- package/.github/dependabot.yml +19 -0
- package/.github/release.yml +36 -0
- package/.github/workflows/build-package.yml +49 -0
- package/.github/workflows/bump-version.yml +59 -0
- package/.github/workflows/dependency-review.yml +26 -0
- package/.github/workflows/publish.yml +155 -0
- package/.github/workflows/stale-activity.yml +27 -0
- package/AGENTS.md +247 -0
- package/CHANGELOG.md +572 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +5 -0
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/SECURITY.md +12 -0
- package/config.schema.json +934 -0
- package/dist/conditions/already-triggered-condition.d.ts +10 -0
- package/dist/conditions/already-triggered-condition.js +22 -0
- package/dist/conditions/already-triggered-condition.js.map +1 -0
- package/dist/conditions/arming-in-progress-condition.d.ts +10 -0
- package/dist/conditions/arming-in-progress-condition.js +21 -0
- package/dist/conditions/arming-in-progress-condition.js.map +1 -0
- package/dist/conditions/arming-lock-condition.d.ts +10 -0
- package/dist/conditions/arming-lock-condition.js +43 -0
- package/dist/conditions/arming-lock-condition.js.map +1 -0
- package/dist/conditions/condition.d.ts +22 -0
- package/dist/conditions/condition.js +19 -0
- package/dist/conditions/condition.js.map +1 -0
- package/dist/conditions/double-knock-condition.d.ts +24 -0
- package/dist/conditions/double-knock-condition.js +73 -0
- package/dist/conditions/double-knock-condition.js.map +1 -0
- package/dist/conditions/not-armed-condition.d.ts +10 -0
- package/dist/conditions/not-armed-condition.js +26 -0
- package/dist/conditions/not-armed-condition.js.map +1 -0
- package/dist/conditions/trigger-already-running-condition.d.ts +10 -0
- package/dist/conditions/trigger-already-running-condition.js +21 -0
- package/dist/conditions/trigger-already-running-condition.js.map +1 -0
- package/dist/constants/default-constant.d.ts +24 -0
- package/dist/constants/default-constant.js +27 -0
- package/dist/constants/default-constant.js.map +1 -0
- package/dist/constants/homekit-constant.d.ts +2 -0
- package/dist/constants/homekit-constant.js +3 -0
- package/dist/constants/homekit-constant.js.map +1 -0
- package/dist/constants/matter-constant.d.ts +21 -0
- package/dist/constants/matter-constant.js +61 -0
- package/dist/constants/matter-constant.js.map +1 -0
- package/dist/handlers/sensor-handler.d.ts +24 -0
- package/dist/handlers/sensor-handler.js +63 -0
- package/dist/handlers/sensor-handler.js.map +1 -0
- package/dist/handlers/state-handler.d.ts +49 -0
- package/dist/handlers/state-handler.js +204 -0
- package/dist/handlers/state-handler.js.map +1 -0
- package/dist/handlers/switch-handler.d.ts +28 -0
- package/dist/handlers/switch-handler.js +95 -0
- package/dist/handlers/switch-handler.js.map +1 -0
- package/dist/handlers/trip-handler.d.ts +44 -0
- package/dist/handlers/trip-handler.js +156 -0
- package/dist/handlers/trip-handler.js.map +1 -0
- package/dist/homekit/homekit-registrar.d.ts +12 -0
- package/dist/homekit/homekit-registrar.js +23 -0
- package/dist/homekit/homekit-registrar.js.map +1 -0
- package/dist/homekit/service-factory.d.ts +12 -0
- package/dist/homekit/service-factory.js +24 -0
- package/dist/homekit/service-factory.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/condition-context-interface.d.ts +14 -0
- package/dist/interfaces/condition-context-interface.js +2 -0
- package/dist/interfaces/condition-context-interface.js.map +1 -0
- package/dist/interfaces/hap-types-interface.d.ts +9 -0
- package/dist/interfaces/hap-types-interface.js +2 -0
- package/dist/interfaces/hap-types-interface.js.map +1 -0
- package/dist/interfaces/mqtt-status-payload-interface.d.ts +6 -0
- package/dist/interfaces/mqtt-status-payload-interface.js +2 -0
- package/dist/interfaces/mqtt-status-payload-interface.js.map +1 -0
- package/dist/interfaces/options-interface.d.ts +87 -0
- package/dist/interfaces/options-interface.js +2 -0
- package/dist/interfaces/options-interface.js.map +1 -0
- package/dist/interfaces/persisted-state-interface.d.ts +6 -0
- package/dist/interfaces/persisted-state-interface.js +2 -0
- package/dist/interfaces/persisted-state-interface.js.map +1 -0
- package/dist/interfaces/service-registry-interface.d.ts +10 -0
- package/dist/interfaces/service-registry-interface.js +2 -0
- package/dist/interfaces/service-registry-interface.js.map +1 -0
- package/dist/interfaces/system-state-interface.d.ts +22 -0
- package/dist/interfaces/system-state-interface.js +2 -0
- package/dist/interfaces/system-state-interface.js.map +1 -0
- package/dist/schemas/arming-lock-schema.d.ts +10 -0
- package/dist/schemas/arming-lock-schema.js +12 -0
- package/dist/schemas/arming-lock-schema.js.map +1 -0
- package/dist/schemas/error-schema.d.ts +4 -0
- package/dist/schemas/error-schema.js +7 -0
- package/dist/schemas/error-schema.js.map +1 -0
- package/dist/schemas/mode-request-schema.d.ts +10 -0
- package/dist/schemas/mode-request-schema.js +18 -0
- package/dist/schemas/mode-request-schema.js.map +1 -0
- package/dist/schemas/status-response-schema.d.ts +7 -0
- package/dist/schemas/status-response-schema.js +10 -0
- package/dist/schemas/status-response-schema.js.map +1 -0
- package/dist/schemas/trip-mode-request-schema.d.ts +9 -0
- package/dist/schemas/trip-mode-request-schema.js +24 -0
- package/dist/schemas/trip-mode-request-schema.js.map +1 -0
- package/dist/security-system-platform.d.ts +19 -0
- package/dist/security-system-platform.js +56 -0
- package/dist/security-system-platform.js.map +1 -0
- package/dist/security-system.d.ts +28 -0
- package/dist/security-system.js +169 -0
- package/dist/security-system.js.map +1 -0
- package/dist/services/command-service.d.ts +16 -0
- package/dist/services/command-service.js +65 -0
- package/dist/services/command-service.js.map +1 -0
- package/dist/services/configuration-service.d.ts +24 -0
- package/dist/services/configuration-service.js +189 -0
- package/dist/services/configuration-service.js.map +1 -0
- package/dist/services/event-bus-service.d.ts +12 -0
- package/dist/services/event-bus-service.js +17 -0
- package/dist/services/event-bus-service.js.map +1 -0
- package/dist/services/matter-service.d.ts +69 -0
- package/dist/services/matter-service.js +363 -0
- package/dist/services/matter-service.js.map +1 -0
- package/dist/services/mqtt-service.d.ts +16 -0
- package/dist/services/mqtt-service.js +97 -0
- package/dist/services/mqtt-service.js.map +1 -0
- package/dist/services/server-service.d.ts +28 -0
- package/dist/services/server-service.js +275 -0
- package/dist/services/server-service.js.map +1 -0
- package/dist/services/storage-service.d.ts +14 -0
- package/dist/services/storage-service.js +70 -0
- package/dist/services/storage-service.js.map +1 -0
- package/dist/services/webhook-service.d.ts +16 -0
- package/dist/services/webhook-service.js +68 -0
- package/dist/services/webhook-service.js.map +1 -0
- package/dist/tests/conditions.test.d.ts +1 -0
- package/dist/tests/conditions.test.js +175 -0
- package/dist/tests/conditions.test.js.map +1 -0
- package/dist/tests/matter-service.test.d.ts +1 -0
- package/dist/tests/matter-service.test.js +475 -0
- package/dist/tests/matter-service.test.js.map +1 -0
- package/dist/tests/mqtt-service.test.d.ts +1 -0
- package/dist/tests/mqtt-service.test.js +168 -0
- package/dist/tests/mqtt-service.test.js.map +1 -0
- package/dist/tests/sensor-handler.test.d.ts +1 -0
- package/dist/tests/sensor-handler.test.js +87 -0
- package/dist/tests/sensor-handler.test.js.map +1 -0
- package/dist/tests/server-service.test.d.ts +1 -0
- package/dist/tests/server-service.test.js +188 -0
- package/dist/tests/server-service.test.js.map +1 -0
- package/dist/tests/state-handler.test.d.ts +1 -0
- package/dist/tests/state-handler.test.js +276 -0
- package/dist/tests/state-handler.test.js.map +1 -0
- package/dist/tests/switch-handler.test.d.ts +1 -0
- package/dist/tests/switch-handler.test.js +186 -0
- package/dist/tests/switch-handler.test.js.map +1 -0
- package/dist/tests/trip-handler.test.d.ts +1 -0
- package/dist/tests/trip-handler.test.js +179 -0
- package/dist/tests/trip-handler.test.js.map +1 -0
- package/dist/timers/timer-manager.d.ts +31 -0
- package/dist/timers/timer-manager.js +118 -0
- package/dist/timers/timer-manager.js.map +1 -0
- package/dist/types/event-payload-map-type.d.ts +15 -0
- package/dist/types/event-payload-map-type.js +2 -0
- package/dist/types/event-payload-map-type.js.map +1 -0
- package/dist/types/event-type.d.ts +44 -0
- package/dist/types/event-type.js +20 -0
- package/dist/types/event-type.js.map +1 -0
- package/dist/types/matter-accessory-key-type.d.ts +7 -0
- package/dist/types/matter-accessory-key-type.js +2 -0
- package/dist/types/matter-accessory-key-type.js.map +1 -0
- package/dist/types/mode-type.d.ts +4 -0
- package/dist/types/mode-type.js +2 -0
- package/dist/types/mode-type.js.map +1 -0
- package/dist/types/origin-type.d.ts +9 -0
- package/dist/types/origin-type.js +11 -0
- package/dist/types/origin-type.js.map +1 -0
- package/dist/types/security-state-type.d.ts +11 -0
- package/dist/types/security-state-type.js +13 -0
- package/dist/types/security-state-type.js.map +1 -0
- package/dist/types/sensor-kind-type.d.ts +2 -0
- package/dist/types/sensor-kind-type.js +2 -0
- package/dist/types/sensor-kind-type.js.map +1 -0
- package/dist/types/service-result-type.d.ts +4 -0
- package/dist/types/service-result-type.js +2 -0
- package/dist/types/service-result-type.js.map +1 -0
- package/dist/utils/arming-util.d.ts +19 -0
- package/dist/utils/arming-util.js +24 -0
- package/dist/utils/arming-util.js.map +1 -0
- package/dist/utils/state-util.d.ts +8 -0
- package/dist/utils/state-util.js +29 -0
- package/dist/utils/state-util.js.map +1 -0
- package/docs/MQTT.md +50 -0
- package/package.json +64 -0
- package/src/@types/homebridge-lib.d.ts +11 -0
- package/src/conditions/already-triggered-condition.ts +24 -0
- package/src/conditions/arming-in-progress-condition.ts +23 -0
- package/src/conditions/arming-lock-condition.ts +50 -0
- package/src/conditions/condition.ts +31 -0
- package/src/conditions/double-knock-condition.ts +84 -0
- package/src/conditions/not-armed-condition.ts +30 -0
- package/src/conditions/trigger-already-running-condition.ts +23 -0
- package/src/constants/default-constant.ts +28 -0
- package/src/constants/homekit-constant.ts +2 -0
- package/src/constants/matter-constant.ts +69 -0
- package/src/handlers/sensor-handler.ts +78 -0
- package/src/handlers/state-handler.ts +257 -0
- package/src/handlers/switch-handler.ts +111 -0
- package/src/handlers/trip-handler.ts +193 -0
- package/src/homekit/homekit-registrar.ts +28 -0
- package/src/homekit/service-factory.ts +35 -0
- package/src/index.ts +9 -0
- package/src/interfaces/condition-context-interface.ts +15 -0
- package/src/interfaces/hap-types-interface.ts +11 -0
- package/src/interfaces/mqtt-status-payload-interface.ts +6 -0
- package/src/interfaces/options-interface.ts +116 -0
- package/src/interfaces/persisted-state-interface.ts +7 -0
- package/src/interfaces/service-registry-interface.ts +11 -0
- package/src/interfaces/system-state-interface.ts +25 -0
- package/src/schemas/arming-lock-schema.ts +12 -0
- package/src/schemas/error-schema.ts +7 -0
- package/src/schemas/mode-request-schema.ts +18 -0
- package/src/schemas/status-response-schema.ts +10 -0
- package/src/schemas/trip-mode-request-schema.ts +24 -0
- package/src/security-system-platform.ts +68 -0
- package/src/security-system.ts +199 -0
- package/src/services/command-service.ts +76 -0
- package/src/services/configuration-service.ts +224 -0
- package/src/services/event-bus-service.ts +21 -0
- package/src/services/matter-service.ts +446 -0
- package/src/services/mqtt-service.ts +123 -0
- package/src/services/server-service.ts +327 -0
- package/src/services/storage-service.ts +80 -0
- package/src/services/webhook-service.ts +77 -0
- package/src/tests/conditions.test.ts +212 -0
- package/src/tests/matter-service.test.ts +610 -0
- package/src/tests/mqtt-service.test.ts +231 -0
- package/src/tests/sensor-handler.test.ts +116 -0
- package/src/tests/server-service.test.ts +238 -0
- package/src/tests/state-handler.test.ts +371 -0
- package/src/tests/switch-handler.test.ts +234 -0
- package/src/tests/trip-handler.test.ts +244 -0
- package/src/timers/timer-manager.ts +141 -0
- package/src/types/event-payload-map-type.ts +25 -0
- package/src/types/event-type.ts +61 -0
- package/src/types/matter-accessory-key-type.ts +33 -0
- package/src/types/mode-type.ts +4 -0
- package/src/types/origin-type.ts +9 -0
- package/src/types/security-state-type.ts +11 -0
- package/src/types/sensor-kind-type.ts +2 -0
- package/src/types/service-result-type.ts +4 -0
- package/src/utils/arming-util.ts +44 -0
- package/src/utils/state-util.ts +32 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { EventType } from '../types/event-type.js';
|
|
3
|
+
import { OriginType } from '../types/origin-type.js';
|
|
4
|
+
import { SecurityState } from '../types/security-state-type.js';
|
|
5
|
+
import { EventBusService } from '../services/event-bus-service.js';
|
|
6
|
+
import type { SystemState } from '../interfaces/system-state-interface.js';
|
|
7
|
+
import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
|
|
8
|
+
import { MqttService } from '../services/mqtt-service.js';
|
|
9
|
+
|
|
10
|
+
// ── Mocks ─────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const mockClient = {
|
|
13
|
+
on: vi.fn().mockReturnThis(),
|
|
14
|
+
publish: vi.fn(),
|
|
15
|
+
connected: true,
|
|
16
|
+
end: vi.fn(),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
vi.mock('mqtt', () => ({
|
|
20
|
+
connect: vi.fn(() => mockClient),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { connect } from 'mqtt';
|
|
24
|
+
|
|
25
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function makeLog() {
|
|
28
|
+
return { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeState(overrides: Partial<SystemState> = {}): SystemState {
|
|
32
|
+
return {
|
|
33
|
+
currentState: SecurityState.OFF,
|
|
34
|
+
targetState: SecurityState.OFF,
|
|
35
|
+
defaultState: SecurityState.OFF,
|
|
36
|
+
availableTargetStates: [SecurityState.HOME, SecurityState.AWAY, SecurityState.NIGHT, SecurityState.OFF],
|
|
37
|
+
isArming: false,
|
|
38
|
+
isTripping: false,
|
|
39
|
+
isKnocked: false,
|
|
40
|
+
serverAuthenticationAttempts: 0,
|
|
41
|
+
pausedCurrentState: null,
|
|
42
|
+
armingLocks: { global: false, home: false, away: false, night: false },
|
|
43
|
+
modeAwayExtended: false,
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeOptions(overrides: Partial<SecuritySystemOptions> = {}): SecuritySystemOptions {
|
|
49
|
+
return {
|
|
50
|
+
mqttBroker: null,
|
|
51
|
+
mqttUsername: null,
|
|
52
|
+
mqttPassword: null,
|
|
53
|
+
mqttTopic: 'security-system/state',
|
|
54
|
+
mqttClientId: null,
|
|
55
|
+
proxyMode: false,
|
|
56
|
+
...overrides,
|
|
57
|
+
} as unknown as SecuritySystemOptions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Tests ──────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
describe('MqttService', () => {
|
|
63
|
+
let log: ReturnType<typeof makeLog>;
|
|
64
|
+
let state: SystemState;
|
|
65
|
+
let bus: EventBusService;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
log = makeLog();
|
|
69
|
+
state = makeState();
|
|
70
|
+
bus = new EventBusService();
|
|
71
|
+
vi.clearAllMocks();
|
|
72
|
+
mockClient.connected = true;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
vi.restoreAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('does nothing when broker is not configured', () => {
|
|
80
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: null }), state);
|
|
81
|
+
service.attachToBus(bus);
|
|
82
|
+
|
|
83
|
+
expect(connect).not.toHaveBeenCalled();
|
|
84
|
+
expect(log.debug).toHaveBeenCalledWith('MQTT broker not configured.');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('connects when broker is configured', () => {
|
|
88
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost:1883' }), state);
|
|
89
|
+
service.attachToBus(bus);
|
|
90
|
+
|
|
91
|
+
expect(connect).toHaveBeenCalledWith('mqtt://localhost:1883', {});
|
|
92
|
+
expect(mockClient.on).toHaveBeenCalledWith('connect', expect.any(Function));
|
|
93
|
+
expect(mockClient.on).toHaveBeenCalledWith('error', expect.any(Function));
|
|
94
|
+
expect(mockClient.on).toHaveBeenCalledWith('close', expect.any(Function));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('passes connection options when provided', () => {
|
|
98
|
+
const options = makeOptions({
|
|
99
|
+
mqttBroker: 'mqtt://broker:1883',
|
|
100
|
+
mqttUsername: 'user',
|
|
101
|
+
mqttPassword: 'pass',
|
|
102
|
+
mqttClientId: 'my-client',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
new MqttService(log as never, options, state).attachToBus(bus);
|
|
106
|
+
|
|
107
|
+
expect(connect).toHaveBeenCalledWith('mqtt://broker:1883', {
|
|
108
|
+
clientId: 'my-client',
|
|
109
|
+
username: 'user',
|
|
110
|
+
password: 'pass',
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('publishes status on TARGET_CHANGED event', () => {
|
|
115
|
+
state.isArming = true;
|
|
116
|
+
state.currentState = SecurityState.HOME;
|
|
117
|
+
state.targetState = SecurityState.AWAY;
|
|
118
|
+
|
|
119
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost' }), state);
|
|
120
|
+
service.attachToBus(bus);
|
|
121
|
+
|
|
122
|
+
bus.emit(EventType.TARGET_CHANGED, { state: SecurityState.AWAY, origin: OriginType.INTERNAL });
|
|
123
|
+
|
|
124
|
+
expect(mockClient.publish).toHaveBeenCalledWith(
|
|
125
|
+
'security-system/state',
|
|
126
|
+
JSON.stringify({
|
|
127
|
+
arming: true,
|
|
128
|
+
current_mode: 'home',
|
|
129
|
+
target_mode: 'away',
|
|
130
|
+
tripped: false,
|
|
131
|
+
}),
|
|
132
|
+
{ qos: 0, retain: true },
|
|
133
|
+
expect.any(Function),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('publishes status on CURRENT_CHANGED event', () => {
|
|
138
|
+
state.currentState = SecurityState.TRIGGERED;
|
|
139
|
+
state.isTripping = true;
|
|
140
|
+
|
|
141
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost' }), state);
|
|
142
|
+
service.attachToBus(bus);
|
|
143
|
+
|
|
144
|
+
bus.emit(EventType.CURRENT_CHANGED, { state: SecurityState.TRIGGERED, origin: OriginType.INTERNAL });
|
|
145
|
+
|
|
146
|
+
expect(mockClient.publish).toHaveBeenCalledTimes(1);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('publishes status on ARMING event', () => {
|
|
150
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost' }), state);
|
|
151
|
+
service.attachToBus(bus);
|
|
152
|
+
|
|
153
|
+
bus.emit(EventType.ARMING, { state: SecurityState.HOME });
|
|
154
|
+
|
|
155
|
+
expect(mockClient.publish).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('publishes status on WARNING event', () => {
|
|
159
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost' }), state);
|
|
160
|
+
service.attachToBus(bus);
|
|
161
|
+
|
|
162
|
+
bus.emit(EventType.WARNING, { origin: OriginType.INTERNAL, triggerSeconds: 30 });
|
|
163
|
+
|
|
164
|
+
expect(mockClient.publish).toHaveBeenCalledTimes(1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('bypasses publish when proxyMode is on and origin is EXTERNAL', () => {
|
|
168
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost', proxyMode: true }), state);
|
|
169
|
+
service.attachToBus(bus);
|
|
170
|
+
|
|
171
|
+
bus.emit(EventType.TARGET_CHANGED, { state: SecurityState.HOME, origin: OriginType.EXTERNAL });
|
|
172
|
+
|
|
173
|
+
expect(mockClient.publish).not.toHaveBeenCalled();
|
|
174
|
+
expect(log.debug).toHaveBeenCalledWith('MQTT publish bypassed (proxy mode).');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('does not bypass publish when proxyMode is on but origin is INTERNAL', () => {
|
|
178
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost', proxyMode: true }), state);
|
|
179
|
+
service.attachToBus(bus);
|
|
180
|
+
|
|
181
|
+
bus.emit(EventType.TARGET_CHANGED, { state: SecurityState.HOME, origin: OriginType.INTERNAL });
|
|
182
|
+
|
|
183
|
+
expect(mockClient.publish).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('does not publish when client is not connected', () => {
|
|
187
|
+
mockClient.connected = false;
|
|
188
|
+
|
|
189
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost' }), state);
|
|
190
|
+
service.attachToBus(bus);
|
|
191
|
+
|
|
192
|
+
bus.emit(EventType.TARGET_CHANGED, { state: SecurityState.HOME, origin: OriginType.INTERNAL });
|
|
193
|
+
|
|
194
|
+
expect(mockClient.publish).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('disconnect ends the client', () => {
|
|
198
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost' }), state);
|
|
199
|
+
service.attachToBus(bus);
|
|
200
|
+
service.disconnect();
|
|
201
|
+
|
|
202
|
+
expect(mockClient.end).toHaveBeenCalledWith(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('uses custom topic when configured', () => {
|
|
206
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost', mqttTopic: 'alarm/status' }), state);
|
|
207
|
+
service.attachToBus(bus);
|
|
208
|
+
|
|
209
|
+
bus.emit(EventType.CURRENT_CHANGED, { state: SecurityState.OFF, origin: OriginType.INTERNAL });
|
|
210
|
+
|
|
211
|
+
expect(mockClient.publish).toHaveBeenCalledWith(
|
|
212
|
+
'alarm/status',
|
|
213
|
+
expect.any(String),
|
|
214
|
+
expect.any(Object),
|
|
215
|
+
expect.any(Function),
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('logs error on publish failure', () => {
|
|
220
|
+
const service = new MqttService(log as never, makeOptions({ mqttBroker: 'mqtt://localhost' }), state);
|
|
221
|
+
service.attachToBus(bus);
|
|
222
|
+
|
|
223
|
+
bus.emit(EventType.CURRENT_CHANGED, { state: SecurityState.OFF, origin: OriginType.INTERNAL });
|
|
224
|
+
|
|
225
|
+
const publishCallback = mockClient.publish.mock.calls[0][3];
|
|
226
|
+
publishCallback(new Error('connection lost'));
|
|
227
|
+
|
|
228
|
+
expect(log.error).toHaveBeenCalledWith('MQTT publish failed (security-system/state)');
|
|
229
|
+
expect(log.error).toHaveBeenCalledWith('Error: connection lost');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { EventBusService } from '../services/event-bus-service.js';
|
|
3
|
+
import { EventType } from '../types/event-type.js';
|
|
4
|
+
import type { EventBusService as EventBusType } from '../services/event-bus-service.js';
|
|
5
|
+
import type { SensorStateChangedPayload } from '../types/event-type.js';
|
|
6
|
+
import { SensorHandler } from '../handlers/sensor-handler.js';
|
|
7
|
+
import { SENSOR_PULSE_MS } from '../constants/homekit-constant.js';
|
|
8
|
+
|
|
9
|
+
function makeLog() {
|
|
10
|
+
return { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function recordedEvents(bus: EventBusType): SensorStateChangedPayload[] {
|
|
14
|
+
const events: SensorStateChangedPayload[] = [];
|
|
15
|
+
bus.on(EventType.SENSOR_STATE_CHANGED, payload => events.push(payload));
|
|
16
|
+
return events;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('SensorHandler', () => {
|
|
20
|
+
let bus: EventBusType;
|
|
21
|
+
let handler: SensorHandler;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
vi.useFakeTimers();
|
|
25
|
+
bus = new EventBusService();
|
|
26
|
+
handler = new SensorHandler(makeLog() as never, bus);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.useRealTimers();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('updateArmingMotionSensor records the value and emits', () => {
|
|
34
|
+
const events = recordedEvents(bus);
|
|
35
|
+
|
|
36
|
+
handler.updateArmingMotionSensor(true);
|
|
37
|
+
|
|
38
|
+
expect(handler.getMotionState('arming')).toBe(true);
|
|
39
|
+
expect(events).toHaveLength(1);
|
|
40
|
+
expect(events[0]).toEqual({ sensor: 'arming', value: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('resetArmingMotionSensor only emits when the sensor is on', () => {
|
|
44
|
+
const events = recordedEvents(bus);
|
|
45
|
+
|
|
46
|
+
handler.resetArmingMotionSensor();
|
|
47
|
+
expect(events).toHaveLength(0);
|
|
48
|
+
|
|
49
|
+
handler.updateArmingMotionSensor(true);
|
|
50
|
+
handler.resetArmingMotionSensor();
|
|
51
|
+
|
|
52
|
+
expect(handler.getMotionState('arming')).toBe(false);
|
|
53
|
+
expect(events).toEqual([
|
|
54
|
+
{ sensor: 'arming', value: true },
|
|
55
|
+
{ sensor: 'arming', value: false },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('setTrippedMotionSensor records the value and emits', () => {
|
|
60
|
+
const events = recordedEvents(bus);
|
|
61
|
+
|
|
62
|
+
handler.setTrippedMotionSensor(true);
|
|
63
|
+
|
|
64
|
+
expect(handler.getMotionState('tripped')).toBe(true);
|
|
65
|
+
expect(events[0]).toEqual({ sensor: 'tripped', value: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('pulseTrippedMotionSensor turns the sensor on then resets it after the pulse', () => {
|
|
69
|
+
const events = recordedEvents(bus);
|
|
70
|
+
|
|
71
|
+
handler.pulseTrippedMotionSensor();
|
|
72
|
+
expect(handler.getMotionState('tripped')).toBe(true);
|
|
73
|
+
|
|
74
|
+
vi.advanceTimersByTime(SENSOR_PULSE_MS);
|
|
75
|
+
|
|
76
|
+
expect(handler.getMotionState('tripped')).toBe(false);
|
|
77
|
+
expect(events).toEqual([
|
|
78
|
+
{ sensor: 'tripped', value: true },
|
|
79
|
+
{ sensor: 'tripped', value: false },
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('pulseResetMotionSensor turns the sensor on then resets it after the pulse', () => {
|
|
84
|
+
const events = recordedEvents(bus);
|
|
85
|
+
|
|
86
|
+
handler.pulseResetMotionSensor();
|
|
87
|
+
expect(handler.getMotionState('reset')).toBe(true);
|
|
88
|
+
|
|
89
|
+
vi.advanceTimersByTime(SENSOR_PULSE_MS);
|
|
90
|
+
|
|
91
|
+
expect(handler.getMotionState('reset')).toBe(false);
|
|
92
|
+
expect(events).toEqual([
|
|
93
|
+
{ sensor: 'reset', value: true },
|
|
94
|
+
{ sensor: 'reset', value: false },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('resetTrippedMotionSensor only emits when the sensor is on', () => {
|
|
99
|
+
const events = recordedEvents(bus);
|
|
100
|
+
|
|
101
|
+
handler.resetTrippedMotionSensor();
|
|
102
|
+
expect(events).toHaveLength(0);
|
|
103
|
+
|
|
104
|
+
handler.setTrippedMotionSensor(true);
|
|
105
|
+
handler.resetTrippedMotionSensor();
|
|
106
|
+
|
|
107
|
+
expect(handler.getMotionState('tripped')).toBe(false);
|
|
108
|
+
expect(events).toHaveLength(2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('getMotionState starts at false for every sensor', () => {
|
|
112
|
+
expect(handler.getMotionState('arming')).toBe(false);
|
|
113
|
+
expect(handler.getMotionState('tripped')).toBe(false);
|
|
114
|
+
expect(handler.getMotionState('reset')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { SecurityState } from '../types/security-state-type.js';
|
|
3
|
+
import { OriginType } from '../types/origin-type.js';
|
|
4
|
+
import type { SystemState } from '../interfaces/system-state-interface.js';
|
|
5
|
+
import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
|
|
6
|
+
import type { ServiceResult } from '../types/service-result-type.js';
|
|
7
|
+
|
|
8
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeLog() {
|
|
11
|
+
return { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makeState(overrides: Partial<SystemState> = {}): SystemState {
|
|
15
|
+
return {
|
|
16
|
+
currentState: SecurityState.OFF,
|
|
17
|
+
targetState: SecurityState.OFF,
|
|
18
|
+
defaultState: SecurityState.OFF,
|
|
19
|
+
availableTargetStates: [SecurityState.HOME, SecurityState.AWAY, SecurityState.NIGHT, SecurityState.OFF],
|
|
20
|
+
isArming: false,
|
|
21
|
+
isTripping: false,
|
|
22
|
+
isKnocked: false,
|
|
23
|
+
serverAuthenticationAttempts: 0,
|
|
24
|
+
pausedCurrentState: null,
|
|
25
|
+
armingLocks: { global: false, home: false, away: false, night: false },
|
|
26
|
+
modeAwayExtended: false,
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeOptions(overrides: Partial<SecuritySystemOptions> = {}): SecuritySystemOptions {
|
|
32
|
+
return {
|
|
33
|
+
serverPort: 8080,
|
|
34
|
+
serverApiKey: null,
|
|
35
|
+
...overrides,
|
|
36
|
+
} as unknown as SecuritySystemOptions;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeStateHandler(result: ServiceResult = { success: true }) {
|
|
40
|
+
return {
|
|
41
|
+
updateTargetState: vi.fn().mockReturnValue(result),
|
|
42
|
+
isTripping: vi.fn().mockReturnValue(false),
|
|
43
|
+
setCurrentState: vi.fn(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function makeTripHandler(result: ServiceResult = { success: true }) {
|
|
48
|
+
return {
|
|
49
|
+
updateTripSwitch: vi.fn().mockReturnValue(result),
|
|
50
|
+
triggerIfModeSet: vi.fn().mockReturnValue(result),
|
|
51
|
+
checkTripConditions: vi.fn().mockReturnValue(result),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeSwitchHandler(result: ServiceResult = { success: true }) {
|
|
56
|
+
return {
|
|
57
|
+
updateArmingLock: vi.fn().mockReturnValue(result),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function makeService(
|
|
62
|
+
stateHandlerResult?: ServiceResult,
|
|
63
|
+
tripHandlerResult?: ServiceResult,
|
|
64
|
+
switchHandlerResult?: ServiceResult,
|
|
65
|
+
stateOverrides?: Partial<SystemState>,
|
|
66
|
+
optionsOverrides?: Partial<SecuritySystemOptions>,
|
|
67
|
+
) {
|
|
68
|
+
const { ServerService } = await import('../services/server-service.js');
|
|
69
|
+
const log = makeLog();
|
|
70
|
+
const state = makeState(stateOverrides);
|
|
71
|
+
const options = makeOptions(optionsOverrides);
|
|
72
|
+
const stateHandler = makeStateHandler(stateHandlerResult);
|
|
73
|
+
const tripHandler = makeTripHandler(tripHandlerResult);
|
|
74
|
+
const switchHandler = makeSwitchHandler(switchHandlerResult);
|
|
75
|
+
|
|
76
|
+
const service = new ServerService(log as any, options, state, stateHandler as any, tripHandler as any, switchHandler as any);
|
|
77
|
+
|
|
78
|
+
return { service, stateHandler, tripHandler, switchHandler, state };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function json(body: unknown, path: string, method = 'PUT') {
|
|
82
|
+
return new Request(`http://localhost${path}`, {
|
|
83
|
+
method,
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify(body),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── PUT /mode/update ──────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
describe('PUT /mode/update', () => {
|
|
92
|
+
it('returns 204 and calls updateTargetState with no delay', async () => {
|
|
93
|
+
const { service, stateHandler } = await makeService();
|
|
94
|
+
const res = await service.app.request(json({ mode: 'home' }, '/mode/update'));
|
|
95
|
+
expect(res.status).toBe(204);
|
|
96
|
+
expect(stateHandler.updateTargetState).toHaveBeenCalledWith(SecurityState.HOME, OriginType.EXTERNAL, 0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns 204 and passes delay to updateTargetState', async () => {
|
|
100
|
+
const { service, stateHandler } = await makeService();
|
|
101
|
+
const res = await service.app.request(json({ mode: 'away', delay: 10 }, '/mode/update'));
|
|
102
|
+
expect(res.status).toBe(204);
|
|
103
|
+
expect(stateHandler.updateTargetState).toHaveBeenCalledWith(SecurityState.AWAY, OriginType.EXTERNAL, 10);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('returns 204 for off mode without delay', async () => {
|
|
107
|
+
const { service, stateHandler } = await makeService();
|
|
108
|
+
const res = await service.app.request(json({ mode: 'off' }, '/mode/update'));
|
|
109
|
+
expect(res.status).toBe(204);
|
|
110
|
+
expect(stateHandler.updateTargetState).toHaveBeenCalledWith(SecurityState.OFF, OriginType.EXTERNAL, 0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('returns 409 when stateHandler rejects the mode change', async () => {
|
|
114
|
+
const { service } = await makeService({ success: false, reason: 'target mode is already set' });
|
|
115
|
+
const res = await service.app.request(json({ mode: 'night' }, '/mode/update'));
|
|
116
|
+
expect(res.status).toBe(409);
|
|
117
|
+
const body = await res.json();
|
|
118
|
+
expect(body.reason).toBe('target mode is already set');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('rejects "triggered" — returns 400 (Zod validation failure)', async () => {
|
|
122
|
+
const { service } = await makeService();
|
|
123
|
+
const res = await service.app.request(json({ mode: 'triggered' }, '/mode/update'));
|
|
124
|
+
expect(res.status).toBe(400);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── POST /mode/trip ───────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe('POST /mode/trip', () => {
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
vi.useRealTimers();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('returns 204 and calls updateTripSwitch when mode and delay are omitted', async () => {
|
|
136
|
+
const { service, tripHandler } = await makeService();
|
|
137
|
+
const res = await service.app.request(json({}, '/mode/trip', 'POST'));
|
|
138
|
+
expect(res.status).toBe(204);
|
|
139
|
+
expect(tripHandler.updateTripSwitch).toHaveBeenCalledWith(true, OriginType.EXTERNAL, false);
|
|
140
|
+
expect(tripHandler.triggerIfModeSet).not.toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('returns 204 and calls updateTripSwitch when mode is null', async () => {
|
|
144
|
+
const { service, tripHandler } = await makeService();
|
|
145
|
+
const res = await service.app.request(json({ mode: null }, '/mode/trip', 'POST'));
|
|
146
|
+
expect(res.status).toBe(204);
|
|
147
|
+
expect(tripHandler.updateTripSwitch).toHaveBeenCalledWith(true, OriginType.EXTERNAL, false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('returns 204 and calls triggerIfModeSet when mode is specified', async () => {
|
|
151
|
+
const { service, tripHandler } = await makeService();
|
|
152
|
+
const res = await service.app.request(json({ mode: 'away' }, '/mode/trip', 'POST'));
|
|
153
|
+
expect(res.status).toBe(204);
|
|
154
|
+
expect(tripHandler.triggerIfModeSet).toHaveBeenCalledWith(SecurityState.AWAY, true);
|
|
155
|
+
expect(tripHandler.updateTripSwitch).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('returns 409 when trip handler rejects without delay', async () => {
|
|
159
|
+
const { service } = await makeService(undefined, { success: false, reason: 'not armed' });
|
|
160
|
+
const res = await service.app.request(json({}, '/mode/trip', 'POST'));
|
|
161
|
+
expect(res.status).toBe(409);
|
|
162
|
+
const body = await res.json();
|
|
163
|
+
expect(body.reason).toBe('not armed');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns 409 when mode does not match (triggerIfModeSet rejects)', async () => {
|
|
167
|
+
const { service } = await makeService(undefined, { success: false, reason: 'mode not set' });
|
|
168
|
+
const res = await service.app.request(json({ mode: 'home' }, '/mode/trip', 'POST'));
|
|
169
|
+
expect(res.status).toBe(409);
|
|
170
|
+
const body = await res.json();
|
|
171
|
+
expect(body.reason).toBe('mode not set');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('returns 204 immediately and calls updateTripSwitch after delay when mode is omitted', async () => {
|
|
175
|
+
vi.useFakeTimers();
|
|
176
|
+
const { service, tripHandler } = await makeService();
|
|
177
|
+
|
|
178
|
+
const res = await service.app.request(json({ delay: 5 }, '/mode/trip', 'POST'));
|
|
179
|
+
expect(res.status).toBe(204);
|
|
180
|
+
expect(tripHandler.checkTripConditions).toHaveBeenCalledWith(true, OriginType.EXTERNAL);
|
|
181
|
+
expect(tripHandler.updateTripSwitch).not.toHaveBeenCalled();
|
|
182
|
+
|
|
183
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
184
|
+
expect(tripHandler.updateTripSwitch).toHaveBeenCalledWith(true, OriginType.EXTERNAL, false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('returns 204 immediately and calls triggerIfModeSet after delay when mode is specified', async () => {
|
|
188
|
+
vi.useFakeTimers();
|
|
189
|
+
const { service, tripHandler } = await makeService();
|
|
190
|
+
|
|
191
|
+
const res = await service.app.request(json({ mode: 'night', delay: 3 }, '/mode/trip', 'POST'));
|
|
192
|
+
expect(res.status).toBe(204);
|
|
193
|
+
expect(tripHandler.triggerIfModeSet).not.toHaveBeenCalled();
|
|
194
|
+
|
|
195
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
196
|
+
expect(tripHandler.triggerIfModeSet).toHaveBeenCalledWith(SecurityState.NIGHT, true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('returns 409 on pre-check failure even when delay is specified', async () => {
|
|
200
|
+
const { service, tripHandler } = await makeService(
|
|
201
|
+
undefined,
|
|
202
|
+
{ success: false, reason: 'arming in progress' },
|
|
203
|
+
);
|
|
204
|
+
const res = await service.app.request(json({ delay: 10 }, '/mode/trip', 'POST'));
|
|
205
|
+
expect(res.status).toBe(409);
|
|
206
|
+
expect(tripHandler.updateTripSwitch).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('treats delay: null the same as no delay', async () => {
|
|
210
|
+
const { service, tripHandler } = await makeService();
|
|
211
|
+
const res = await service.app.request(json({ delay: null }, '/mode/trip', 'POST'));
|
|
212
|
+
expect(res.status).toBe(204);
|
|
213
|
+
expect(tripHandler.updateTripSwitch).toHaveBeenCalledWith(true, OriginType.EXTERNAL, false);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ── PUT /switches/arming-lock ─────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe('PUT /switches/arming-lock', () => {
|
|
220
|
+
it('returns 204 and calls updateArmingLock', async () => {
|
|
221
|
+
const { service, switchHandler } = await makeService();
|
|
222
|
+
const res = await service.app.request(json({ mode: 'home', value: true }, '/switches/arming-lock'));
|
|
223
|
+
expect(res.status).toBe(204);
|
|
224
|
+
expect(switchHandler.updateArmingLock).toHaveBeenCalledWith('home', true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns 409 when switchHandler rejects', async () => {
|
|
228
|
+
const { service } = await makeService(
|
|
229
|
+
undefined,
|
|
230
|
+
undefined,
|
|
231
|
+
{ success: false, reason: 'unknown arming lock mode: invalid' },
|
|
232
|
+
);
|
|
233
|
+
const res = await service.app.request(json({ mode: 'global', value: false }, '/switches/arming-lock'));
|
|
234
|
+
expect(res.status).toBe(409);
|
|
235
|
+
const body = await res.json();
|
|
236
|
+
expect(body.reason).toBe('unknown arming lock mode: invalid');
|
|
237
|
+
});
|
|
238
|
+
});
|