homebridge-securitysystem 11.0.0-beta.3 → 11.0.0-beta.5
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/.github/workflows/build-package.yml +3 -1
- package/CHANGELOG.md +27 -0
- package/dist/schemas/mode-request-schema.d.ts +0 -1
- package/dist/schemas/mode-request-schema.js +2 -2
- package/dist/schemas/mode-request-schema.js.map +1 -1
- 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/services/server-service.d.ts +3 -0
- package/dist/services/server-service.js +71 -19
- package/dist/services/server-service.js.map +1 -1
- 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/package.json +1 -1
- package/src/schemas/mode-request-schema.ts +2 -2
- package/src/schemas/trip-mode-request-schema.ts +24 -0
- package/src/services/server-service.ts +83 -21
- package/src/tests/server-service.test.ts +238 -0
|
@@ -7,13 +7,14 @@ on:
|
|
|
7
7
|
paths-ignore:
|
|
8
8
|
- "**.md"
|
|
9
9
|
pull_request:
|
|
10
|
-
types: [opened, reopened, synchronize, ready_for_review]
|
|
10
|
+
types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled]
|
|
11
11
|
paths-ignore:
|
|
12
12
|
- "**.md"
|
|
13
13
|
|
|
14
14
|
jobs:
|
|
15
15
|
build-package:
|
|
16
16
|
name: Build package
|
|
17
|
+
if: ${{ !contains(github.event.pull_request.labels.*.name, 'ignore-for-release') }}
|
|
17
18
|
runs-on: ubuntu-latest
|
|
18
19
|
permissions:
|
|
19
20
|
contents: read
|
|
@@ -35,6 +36,7 @@ jobs:
|
|
|
35
36
|
- name: Build package
|
|
36
37
|
run: |
|
|
37
38
|
npm ci
|
|
39
|
+
npm run lint
|
|
38
40
|
npm pack
|
|
39
41
|
|
|
40
42
|
- name: Attest build provenance
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v11.0.0-beta.4 - 2026-04-19
|
|
4
|
+
|
|
5
|
+
<!-- Release notes generated using configuration in .github/release.yml at v11.0.0-beta.4 -->
|
|
6
|
+
|
|
7
|
+
## What's Changed
|
|
8
|
+
### 🛠️ Other Changes
|
|
9
|
+
* Separate trip endpoint from mode update endpoint by @MiguelRipoll23 in https://github.com/MiguelRipoll23/homebridge-securitysystem/pull/766
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
**Full Changelog**: https://github.com/MiguelRipoll23/homebridge-securitysystem/compare/v11.0.0-beta.3...v11.0.0-beta.4
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## v11.0.0-beta.3 - 2026-04-19
|
|
16
|
+
|
|
17
|
+
<!-- Release notes generated using configuration in .github/release.yml at v11.0.0-beta.3 -->
|
|
18
|
+
|
|
19
|
+
## What's Changed
|
|
20
|
+
### ✨ Enhancements
|
|
21
|
+
* refactor: replace boolean returns with ServiceResult objects and add condition failure reasons by @MiguelRipoll23 in https://github.com/MiguelRipoll23/homebridge-securitysystem/pull/764
|
|
22
|
+
### 🛠️ Other Changes
|
|
23
|
+
* fix: update server configuration to use API key instead of server code by @MiguelRipoll23 in https://github.com/MiguelRipoll23/homebridge-securitysystem/pull/762
|
|
24
|
+
* Improve API authentication logging and naming clarity by @MiguelRipoll23 in https://github.com/MiguelRipoll23/homebridge-securitysystem/pull/763
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
**Full Changelog**: https://github.com/MiguelRipoll23/homebridge-securitysystem/compare/v11.0.0-beta.2...v11.0.0-beta.3
|
|
28
|
+
|
|
29
|
+
|
|
3
30
|
## v11.0.0-beta.2 - 2026-04-18
|
|
4
31
|
|
|
5
32
|
<!-- Release notes generated using configuration in .github/release.yml at v11.0.0-beta.2 -->
|
|
@@ -2,8 +2,8 @@ import { z } from '@hono/zod-openapi';
|
|
|
2
2
|
export const ModeRequestSchema = z
|
|
3
3
|
.object({
|
|
4
4
|
mode: z
|
|
5
|
-
.enum(['home', 'away', 'night', 'off'
|
|
6
|
-
.openapi({ example: 'home', description: 'Target
|
|
5
|
+
.enum(['home', 'away', 'night', 'off'])
|
|
6
|
+
.openapi({ example: 'home', description: 'Target mode to activate (home, away, night, or off)' }),
|
|
7
7
|
delay: z
|
|
8
8
|
.number()
|
|
9
9
|
.int()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mode-request-schema.js","sourceRoot":"","sources":["../../src/schemas/mode-request-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAC;AAEtC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC;IACN,IAAI,EAAE,CAAC;SACJ,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"mode-request-schema.js","sourceRoot":"","sources":["../../src/schemas/mode-request-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAC;AAEtC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC;IACN,IAAI,EAAE,CAAC;SACJ,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;SACtC,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,qDAAqD,EAAE,CAAC;IACnG,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,EAAE;SACL,WAAW,EAAE;SACb,QAAQ,EAAE;SACV,OAAO,CAAC;QACP,OAAO,EAAE,CAAC;QACV,WAAW,EAAE,2DAA2D;KACzE,CAAC;CACL,CAAC;KACD,OAAO,CAAC,aAAa,CAAC,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from '@hono/zod-openapi';
|
|
2
|
+
export declare const TripModeRequestSchema: z.ZodObject<{
|
|
3
|
+
mode: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
4
|
+
home: "home";
|
|
5
|
+
away: "away";
|
|
6
|
+
night: "night";
|
|
7
|
+
}>>>;
|
|
8
|
+
delay: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
9
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from '@hono/zod-openapi';
|
|
2
|
+
export const TripModeRequestSchema = z
|
|
3
|
+
.object({
|
|
4
|
+
mode: z
|
|
5
|
+
.enum(['home', 'away', 'night'])
|
|
6
|
+
.nullable()
|
|
7
|
+
.optional()
|
|
8
|
+
.openapi({
|
|
9
|
+
example: 'home',
|
|
10
|
+
description: 'Mode to trip. If omitted or null, the action applies to all modes.',
|
|
11
|
+
}),
|
|
12
|
+
delay: z
|
|
13
|
+
.number()
|
|
14
|
+
.int()
|
|
15
|
+
.nonnegative()
|
|
16
|
+
.nullable()
|
|
17
|
+
.optional()
|
|
18
|
+
.openapi({
|
|
19
|
+
example: 5,
|
|
20
|
+
description: 'Delay in seconds before activating the alarm.',
|
|
21
|
+
}),
|
|
22
|
+
})
|
|
23
|
+
.openapi('TripModeRequest');
|
|
24
|
+
//# sourceMappingURL=trip-mode-request-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trip-mode-request-schema.js","sourceRoot":"","sources":["../../src/schemas/trip-mode-request-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,mBAAmB,CAAC;AAEtC,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC;KACnC,MAAM,CAAC;IACN,IAAI,EAAE,CAAC;SACJ,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;SAC/B,QAAQ,EAAE;SACV,QAAQ,EAAE;SACV,OAAO,CAAC;QACP,OAAO,EAAE,MAAM;QACf,WAAW,EAAE,oEAAoE;KAClF,CAAC;IACJ,KAAK,EAAE,CAAC;SACL,MAAM,EAAE;SACR,GAAG,EAAE;SACL,WAAW,EAAE;SACb,QAAQ,EAAE;SACV,QAAQ,EAAE;SACV,OAAO,CAAC;QACP,OAAO,EAAE,CAAC;QACV,WAAW,EAAE,+CAA+C;KAC7D,CAAC;CACL,CAAC;KACD,OAAO,CAAC,iBAAiB,CAAC,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OpenAPIHono } from '@hono/zod-openapi';
|
|
1
2
|
import type { Logging } from 'homebridge';
|
|
2
3
|
import type { SecuritySystemOptions } from '../interfaces/options-interface.js';
|
|
3
4
|
import type { SystemState } from '../interfaces/system-state-interface.js';
|
|
@@ -14,6 +15,8 @@ export declare class ServerService {
|
|
|
14
15
|
private readonly switchHandler;
|
|
15
16
|
private readonly application;
|
|
16
17
|
constructor(log: Logging, options: SecuritySystemOptions, state: SystemState, stateHandler: StateHandler, tripHandler: TripHandler, switchHandler: SwitchHandler);
|
|
18
|
+
/** Exposed for testing — returns the underlying Hono app. */
|
|
19
|
+
get app(): OpenAPIHono;
|
|
17
20
|
start(): void;
|
|
18
21
|
private createAuthMiddleware;
|
|
19
22
|
private registerRoutes;
|
|
@@ -7,6 +7,7 @@ import { stateToMode } from '../utils/state-util.js';
|
|
|
7
7
|
import { ErrorSchema } from '../schemas/error-schema.js';
|
|
8
8
|
import { StatusResponseSchema } from '../schemas/status-response-schema.js';
|
|
9
9
|
import { ModeRequestSchema } from '../schemas/mode-request-schema.js';
|
|
10
|
+
import { TripModeRequestSchema } from '../schemas/trip-mode-request-schema.js';
|
|
10
11
|
import { ArmingLockRequestSchema } from '../schemas/arming-lock-schema.js';
|
|
11
12
|
const MODE_TO_STATE = {
|
|
12
13
|
home: SecurityState.HOME,
|
|
@@ -40,10 +41,11 @@ const statusRoute = createRoute({
|
|
|
40
41
|
});
|
|
41
42
|
const modeRoute = createRoute({
|
|
42
43
|
method: 'put',
|
|
43
|
-
path: '/mode',
|
|
44
|
-
summary: 'Change
|
|
45
|
-
description: 'Sets the target security mode. Supported modes: home, away, night, off
|
|
46
|
-
'
|
|
44
|
+
path: '/mode/update',
|
|
45
|
+
summary: 'Change mode',
|
|
46
|
+
description: 'Sets the target security mode. Supported modes: home, away, night, off. ' +
|
|
47
|
+
'An optional delay (seconds) defers the transition. ' +
|
|
48
|
+
'To trigger the alarm use the POST /mode/trip endpoint instead.',
|
|
47
49
|
security: [{ BearerAuth: [] }],
|
|
48
50
|
request: {
|
|
49
51
|
body: {
|
|
@@ -65,10 +67,38 @@ const modeRoute = createRoute({
|
|
|
65
67
|
},
|
|
66
68
|
},
|
|
67
69
|
});
|
|
70
|
+
const tripModeRoute = createRoute({
|
|
71
|
+
method: 'post',
|
|
72
|
+
path: '/mode/trip',
|
|
73
|
+
summary: 'Trip mode',
|
|
74
|
+
description: 'Activates the alarm ("triggered" state). ' +
|
|
75
|
+
'If mode is specified the alarm only fires when the system is currently in that mode. ' +
|
|
76
|
+
'An optional delay (seconds) defers the activation.',
|
|
77
|
+
security: [{ BearerAuth: [] }],
|
|
78
|
+
request: {
|
|
79
|
+
body: {
|
|
80
|
+
content: {
|
|
81
|
+
'application/json': {
|
|
82
|
+
schema: TripModeRequestSchema,
|
|
83
|
+
example: { mode: 'home', delay: 30 },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: true,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
responses: {
|
|
90
|
+
204: { description: 'Trip accepted' },
|
|
91
|
+
...AUTH_RESPONSES,
|
|
92
|
+
409: {
|
|
93
|
+
content: { 'application/json': { schema: ErrorSchema } },
|
|
94
|
+
description: 'Trip rejected by the system',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
});
|
|
68
98
|
const armingLockRoute = createRoute({
|
|
69
99
|
method: 'put',
|
|
70
100
|
path: '/switches/arming-lock',
|
|
71
|
-
summary: 'Update arming lock
|
|
101
|
+
summary: 'Update arming lock',
|
|
72
102
|
description: 'Enables or disables the arming lock for a specific mode or globally.',
|
|
73
103
|
security: [{ BearerAuth: [] }],
|
|
74
104
|
request: {
|
|
@@ -107,9 +137,13 @@ export class ServerService {
|
|
|
107
137
|
this.stateHandler = stateHandler;
|
|
108
138
|
this.tripHandler = tripHandler;
|
|
109
139
|
this.switchHandler = switchHandler;
|
|
140
|
+
this.registerRoutes();
|
|
141
|
+
}
|
|
142
|
+
/** Exposed for testing — returns the underlying Hono app. */
|
|
143
|
+
get app() {
|
|
144
|
+
return this.application;
|
|
110
145
|
}
|
|
111
146
|
start() {
|
|
112
|
-
this.registerRoutes();
|
|
113
147
|
const server = serve({ fetch: this.application.fetch, port: this.options.serverPort }, () => this.log.info(`Server (${this.options.serverPort})`));
|
|
114
148
|
server.on('error', (error) => {
|
|
115
149
|
this.log.error('Server error.');
|
|
@@ -158,27 +192,45 @@ export class ServerService {
|
|
|
158
192
|
tripped: this.stateHandler.isTripping(),
|
|
159
193
|
});
|
|
160
194
|
});
|
|
161
|
-
// PUT /mode
|
|
162
|
-
this.application.use('/mode', auth);
|
|
195
|
+
// PUT /mode/update
|
|
196
|
+
this.application.use('/mode/update', auth);
|
|
163
197
|
this.application.openapi(modeRoute, (c) => {
|
|
164
198
|
const { mode, delay = 0 } = c.req.valid('json');
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
|
|
168
|
-
|
|
199
|
+
const result = this.stateHandler.updateTargetState(MODE_TO_STATE[mode], OriginType.EXTERNAL, delay);
|
|
200
|
+
if (!result.success) {
|
|
201
|
+
return c.json({ reason: result.reason ?? 'Mode change rejected by the system' }, 409);
|
|
202
|
+
}
|
|
203
|
+
return c.body(null, 204);
|
|
204
|
+
});
|
|
205
|
+
// POST /mode/trip
|
|
206
|
+
this.application.use('/mode/trip', auth);
|
|
207
|
+
this.application.openapi(tripModeRoute, (c) => {
|
|
208
|
+
const { mode, delay } = c.req.valid('json');
|
|
209
|
+
const effectiveDelay = delay ?? 0;
|
|
210
|
+
if (effectiveDelay > 0) {
|
|
211
|
+
const precheck = this.tripHandler.checkTripConditions(true, OriginType.EXTERNAL);
|
|
212
|
+
if (!precheck.success) {
|
|
213
|
+
return c.json({ reason: precheck.reason ?? 'Trip rejected by the system' }, 409);
|
|
169
214
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
this.stateHandler.setCurrentState(SecurityState.TRIGGERED, OriginType.EXTERNAL);
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
if (mode != null) {
|
|
217
|
+
this.tripHandler.triggerIfModeSet(MODE_TO_STATE[mode], true);
|
|
174
218
|
}
|
|
175
|
-
|
|
219
|
+
else {
|
|
220
|
+
this.tripHandler.updateTripSwitch(true, OriginType.EXTERNAL, false);
|
|
221
|
+
}
|
|
222
|
+
}, effectiveDelay * 1000);
|
|
223
|
+
return c.body(null, 204);
|
|
224
|
+
}
|
|
225
|
+
let result;
|
|
226
|
+
if (mode != null) {
|
|
227
|
+
result = this.tripHandler.triggerIfModeSet(MODE_TO_STATE[mode], true);
|
|
176
228
|
}
|
|
177
229
|
else {
|
|
178
|
-
result = this.
|
|
230
|
+
result = this.tripHandler.updateTripSwitch(true, OriginType.EXTERNAL, false);
|
|
179
231
|
}
|
|
180
232
|
if (!result.success) {
|
|
181
|
-
return c.json({ reason: result.reason ?? '
|
|
233
|
+
return c.json({ reason: result.reason ?? 'Trip rejected by the system' }, 409);
|
|
182
234
|
}
|
|
183
235
|
return c.body(null, 204);
|
|
184
236
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-service.js","sourceRoot":"","sources":["../../src/services/server-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAGpD,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAMrD,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,kCAAkC,CAAC;AAG3E,MAAM,aAAa,GAAkC;IACnD,IAAI,EAAE,aAAa,CAAC,IAAI;IACxB,IAAI,EAAE,aAAa,CAAC,IAAI;IACxB,KAAK,EAAE,aAAa,CAAC,KAAK;IAC1B,GAAG,EAAE,aAAa,CAAC,GAAG;CACvB,CAAC;AAEF,MAAM,cAAc,GAAG;IACrB,GAAG,EAAE;QACH,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE;QACxD,WAAW,EAAE,kBAAkB;KAChC;IACD,GAAG,EAAE;QACH,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE;QACxD,WAAW,EAAE,4BAA4B;KAC1C;CACO,CAAC;AAEX,MAAM,WAAW,GAAG,WAAW,CAAC;IAC9B,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,QAAQ;IACd,OAAO,EAAE,WAAW;IACpB,WAAW,EAAE,8EAA8E;IAC3F,QAAQ,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;IAC9B,SAAS,EAAE;QACT,GAAG,EAAE;YACH,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,oBAAoB,EAAE,EAAE;YACjE,WAAW,EAAE,sBAAsB;SACpC;QACD,GAAG,cAAc;KAClB;CACF,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,WAAW,CAAC;IAC5B,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"server-service.js","sourceRoot":"","sources":["../../src/services/server-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
5
|
+
function makeLog() {
|
|
6
|
+
return { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
7
|
+
}
|
|
8
|
+
function makeState(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
currentState: SecurityState.OFF,
|
|
11
|
+
targetState: SecurityState.OFF,
|
|
12
|
+
defaultState: SecurityState.OFF,
|
|
13
|
+
availableTargetStates: [SecurityState.HOME, SecurityState.AWAY, SecurityState.NIGHT, SecurityState.OFF],
|
|
14
|
+
isArming: false,
|
|
15
|
+
isTripping: false,
|
|
16
|
+
isKnocked: false,
|
|
17
|
+
serverAuthenticationAttempts: 0,
|
|
18
|
+
pausedCurrentState: null,
|
|
19
|
+
audioProcess: null,
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function makeOptions(overrides = {}) {
|
|
24
|
+
return {
|
|
25
|
+
serverPort: 8080,
|
|
26
|
+
serverApiKey: null,
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function makeStateHandler(result = { success: true }) {
|
|
31
|
+
return {
|
|
32
|
+
updateTargetState: vi.fn().mockReturnValue(result),
|
|
33
|
+
isTripping: vi.fn().mockReturnValue(false),
|
|
34
|
+
setCurrentState: vi.fn(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function makeTripHandler(result = { success: true }) {
|
|
38
|
+
return {
|
|
39
|
+
updateTripSwitch: vi.fn().mockReturnValue(result),
|
|
40
|
+
triggerIfModeSet: vi.fn().mockReturnValue(result),
|
|
41
|
+
checkTripConditions: vi.fn().mockReturnValue(result),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function makeSwitchHandler(result = { success: true }) {
|
|
45
|
+
return {
|
|
46
|
+
updateArmingLock: vi.fn().mockReturnValue(result),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
async function makeService(stateHandlerResult, tripHandlerResult, switchHandlerResult, stateOverrides, optionsOverrides) {
|
|
50
|
+
const { ServerService } = await import('../services/server-service.js');
|
|
51
|
+
const log = makeLog();
|
|
52
|
+
const state = makeState(stateOverrides);
|
|
53
|
+
const options = makeOptions(optionsOverrides);
|
|
54
|
+
const stateHandler = makeStateHandler(stateHandlerResult);
|
|
55
|
+
const tripHandler = makeTripHandler(tripHandlerResult);
|
|
56
|
+
const switchHandler = makeSwitchHandler(switchHandlerResult);
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
const service = new ServerService(log, options, state, stateHandler, tripHandler, switchHandler);
|
|
59
|
+
return { service, stateHandler, tripHandler, switchHandler, state };
|
|
60
|
+
}
|
|
61
|
+
function json(body, path, method = 'PUT') {
|
|
62
|
+
return new Request(`http://localhost${path}`, {
|
|
63
|
+
method,
|
|
64
|
+
headers: { 'Content-Type': 'application/json' },
|
|
65
|
+
body: JSON.stringify(body),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// ── PUT /mode/update ──────────────────────────────────────────────────────────
|
|
69
|
+
describe('PUT /mode/update', () => {
|
|
70
|
+
it('returns 204 and calls updateTargetState with no delay', async () => {
|
|
71
|
+
const { service, stateHandler } = await makeService();
|
|
72
|
+
const res = await service.app.request(json({ mode: 'home' }, '/mode/update'));
|
|
73
|
+
expect(res.status).toBe(204);
|
|
74
|
+
expect(stateHandler.updateTargetState).toHaveBeenCalledWith(SecurityState.HOME, OriginType.EXTERNAL, 0);
|
|
75
|
+
});
|
|
76
|
+
it('returns 204 and passes delay to updateTargetState', async () => {
|
|
77
|
+
const { service, stateHandler } = await makeService();
|
|
78
|
+
const res = await service.app.request(json({ mode: 'away', delay: 10 }, '/mode/update'));
|
|
79
|
+
expect(res.status).toBe(204);
|
|
80
|
+
expect(stateHandler.updateTargetState).toHaveBeenCalledWith(SecurityState.AWAY, OriginType.EXTERNAL, 10);
|
|
81
|
+
});
|
|
82
|
+
it('returns 204 for off mode without delay', async () => {
|
|
83
|
+
const { service, stateHandler } = await makeService();
|
|
84
|
+
const res = await service.app.request(json({ mode: 'off' }, '/mode/update'));
|
|
85
|
+
expect(res.status).toBe(204);
|
|
86
|
+
expect(stateHandler.updateTargetState).toHaveBeenCalledWith(SecurityState.OFF, OriginType.EXTERNAL, 0);
|
|
87
|
+
});
|
|
88
|
+
it('returns 409 when stateHandler rejects the mode change', async () => {
|
|
89
|
+
const { service } = await makeService({ success: false, reason: 'target mode is already set' });
|
|
90
|
+
const res = await service.app.request(json({ mode: 'night' }, '/mode/update'));
|
|
91
|
+
expect(res.status).toBe(409);
|
|
92
|
+
const body = await res.json();
|
|
93
|
+
expect(body.reason).toBe('target mode is already set');
|
|
94
|
+
});
|
|
95
|
+
it('rejects "triggered" — returns 400 (Zod validation failure)', async () => {
|
|
96
|
+
const { service } = await makeService();
|
|
97
|
+
const res = await service.app.request(json({ mode: 'triggered' }, '/mode/update'));
|
|
98
|
+
expect(res.status).toBe(400);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
// ── POST /mode/trip ───────────────────────────────────────────────────────────
|
|
102
|
+
describe('POST /mode/trip', () => {
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
vi.useRealTimers();
|
|
105
|
+
});
|
|
106
|
+
it('returns 204 and calls updateTripSwitch when mode and delay are omitted', async () => {
|
|
107
|
+
const { service, tripHandler } = await makeService();
|
|
108
|
+
const res = await service.app.request(json({}, '/mode/trip', 'POST'));
|
|
109
|
+
expect(res.status).toBe(204);
|
|
110
|
+
expect(tripHandler.updateTripSwitch).toHaveBeenCalledWith(true, OriginType.EXTERNAL, false);
|
|
111
|
+
expect(tripHandler.triggerIfModeSet).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
it('returns 204 and calls updateTripSwitch when mode is null', async () => {
|
|
114
|
+
const { service, tripHandler } = await makeService();
|
|
115
|
+
const res = await service.app.request(json({ mode: null }, '/mode/trip', 'POST'));
|
|
116
|
+
expect(res.status).toBe(204);
|
|
117
|
+
expect(tripHandler.updateTripSwitch).toHaveBeenCalledWith(true, OriginType.EXTERNAL, false);
|
|
118
|
+
});
|
|
119
|
+
it('returns 204 and calls triggerIfModeSet when mode is specified', async () => {
|
|
120
|
+
const { service, tripHandler } = await makeService();
|
|
121
|
+
const res = await service.app.request(json({ mode: 'away' }, '/mode/trip', 'POST'));
|
|
122
|
+
expect(res.status).toBe(204);
|
|
123
|
+
expect(tripHandler.triggerIfModeSet).toHaveBeenCalledWith(SecurityState.AWAY, true);
|
|
124
|
+
expect(tripHandler.updateTripSwitch).not.toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
it('returns 409 when trip handler rejects without delay', async () => {
|
|
127
|
+
const { service } = await makeService(undefined, { success: false, reason: 'not armed' });
|
|
128
|
+
const res = await service.app.request(json({}, '/mode/trip', 'POST'));
|
|
129
|
+
expect(res.status).toBe(409);
|
|
130
|
+
const body = await res.json();
|
|
131
|
+
expect(body.reason).toBe('not armed');
|
|
132
|
+
});
|
|
133
|
+
it('returns 409 when mode does not match (triggerIfModeSet rejects)', async () => {
|
|
134
|
+
const { service } = await makeService(undefined, { success: false, reason: 'mode not set' });
|
|
135
|
+
const res = await service.app.request(json({ mode: 'home' }, '/mode/trip', 'POST'));
|
|
136
|
+
expect(res.status).toBe(409);
|
|
137
|
+
const body = await res.json();
|
|
138
|
+
expect(body.reason).toBe('mode not set');
|
|
139
|
+
});
|
|
140
|
+
it('returns 204 immediately and calls updateTripSwitch after delay when mode is omitted', async () => {
|
|
141
|
+
vi.useFakeTimers();
|
|
142
|
+
const { service, tripHandler } = await makeService();
|
|
143
|
+
const res = await service.app.request(json({ delay: 5 }, '/mode/trip', 'POST'));
|
|
144
|
+
expect(res.status).toBe(204);
|
|
145
|
+
expect(tripHandler.checkTripConditions).toHaveBeenCalledWith(true, OriginType.EXTERNAL);
|
|
146
|
+
expect(tripHandler.updateTripSwitch).not.toHaveBeenCalled();
|
|
147
|
+
await vi.advanceTimersByTimeAsync(5000);
|
|
148
|
+
expect(tripHandler.updateTripSwitch).toHaveBeenCalledWith(true, OriginType.EXTERNAL, false);
|
|
149
|
+
});
|
|
150
|
+
it('returns 204 immediately and calls triggerIfModeSet after delay when mode is specified', async () => {
|
|
151
|
+
vi.useFakeTimers();
|
|
152
|
+
const { service, tripHandler } = await makeService();
|
|
153
|
+
const res = await service.app.request(json({ mode: 'night', delay: 3 }, '/mode/trip', 'POST'));
|
|
154
|
+
expect(res.status).toBe(204);
|
|
155
|
+
expect(tripHandler.triggerIfModeSet).not.toHaveBeenCalled();
|
|
156
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
157
|
+
expect(tripHandler.triggerIfModeSet).toHaveBeenCalledWith(SecurityState.NIGHT, true);
|
|
158
|
+
});
|
|
159
|
+
it('returns 409 on pre-check failure even when delay is specified', async () => {
|
|
160
|
+
const { service, tripHandler } = await makeService(undefined, { success: false, reason: 'arming in progress' });
|
|
161
|
+
const res = await service.app.request(json({ delay: 10 }, '/mode/trip', 'POST'));
|
|
162
|
+
expect(res.status).toBe(409);
|
|
163
|
+
expect(tripHandler.updateTripSwitch).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
it('treats delay: null the same as no delay', async () => {
|
|
166
|
+
const { service, tripHandler } = await makeService();
|
|
167
|
+
const res = await service.app.request(json({ delay: null }, '/mode/trip', 'POST'));
|
|
168
|
+
expect(res.status).toBe(204);
|
|
169
|
+
expect(tripHandler.updateTripSwitch).toHaveBeenCalledWith(true, OriginType.EXTERNAL, false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
// ── PUT /switches/arming-lock ─────────────────────────────────────────────────
|
|
173
|
+
describe('PUT /switches/arming-lock', () => {
|
|
174
|
+
it('returns 204 and calls updateArmingLock', async () => {
|
|
175
|
+
const { service, switchHandler } = await makeService();
|
|
176
|
+
const res = await service.app.request(json({ mode: 'home', value: true }, '/switches/arming-lock'));
|
|
177
|
+
expect(res.status).toBe(204);
|
|
178
|
+
expect(switchHandler.updateArmingLock).toHaveBeenCalledWith('home', true);
|
|
179
|
+
});
|
|
180
|
+
it('returns 409 when switchHandler rejects', async () => {
|
|
181
|
+
const { service } = await makeService(undefined, undefined, { success: false, reason: 'unknown arming lock mode: invalid' });
|
|
182
|
+
const res = await service.app.request(json({ mode: 'global', value: false }, '/switches/arming-lock'));
|
|
183
|
+
expect(res.status).toBe(409);
|
|
184
|
+
const body = await res.json();
|
|
185
|
+
expect(body.reason).toBe('unknown arming lock mode: invalid');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=server-service.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-service.test.js","sourceRoot":"","sources":["../../src/tests/server-service.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "homebridge-securitysystem",
|
|
3
3
|
"displayName": "Homebridge Security System",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "11.0.0-beta.
|
|
5
|
+
"version": "11.0.0-beta.5",
|
|
6
6
|
"description": "Homebridge plugin that creates a security system accessory that can be triggered by HomeKit sensors.",
|
|
7
7
|
"author": "MiguelRipoll23",
|
|
8
8
|
"license": "MIT",
|
|
@@ -3,8 +3,8 @@ import { z } from '@hono/zod-openapi';
|
|
|
3
3
|
export const ModeRequestSchema = z
|
|
4
4
|
.object({
|
|
5
5
|
mode: z
|
|
6
|
-
.enum(['home', 'away', 'night', 'off'
|
|
7
|
-
.openapi({ example: 'home', description: 'Target
|
|
6
|
+
.enum(['home', 'away', 'night', 'off'])
|
|
7
|
+
.openapi({ example: 'home', description: 'Target mode to activate (home, away, night, or off)' }),
|
|
8
8
|
delay: z
|
|
9
9
|
.number()
|
|
10
10
|
.int()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from '@hono/zod-openapi';
|
|
2
|
+
|
|
3
|
+
export const TripModeRequestSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
mode: z
|
|
6
|
+
.enum(['home', 'away', 'night'])
|
|
7
|
+
.nullable()
|
|
8
|
+
.optional()
|
|
9
|
+
.openapi({
|
|
10
|
+
example: 'home',
|
|
11
|
+
description: 'Mode to trip. If omitted or null, the action applies to all modes.',
|
|
12
|
+
}),
|
|
13
|
+
delay: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.nonnegative()
|
|
17
|
+
.nullable()
|
|
18
|
+
.optional()
|
|
19
|
+
.openapi({
|
|
20
|
+
example: 5,
|
|
21
|
+
description: 'Delay in seconds before activating the alarm.',
|
|
22
|
+
}),
|
|
23
|
+
})
|
|
24
|
+
.openapi('TripModeRequest');
|
|
@@ -14,6 +14,7 @@ import type { SwitchHandler } from '../handlers/switch-handler.js';
|
|
|
14
14
|
import { ErrorSchema } from '../schemas/error-schema.js';
|
|
15
15
|
import { StatusResponseSchema } from '../schemas/status-response-schema.js';
|
|
16
16
|
import { ModeRequestSchema } from '../schemas/mode-request-schema.js';
|
|
17
|
+
import { TripModeRequestSchema } from '../schemas/trip-mode-request-schema.js';
|
|
17
18
|
import { ArmingLockRequestSchema } from '../schemas/arming-lock-schema.js';
|
|
18
19
|
import type { ServiceResult } from '../types/service-result-type.js';
|
|
19
20
|
|
|
@@ -52,11 +53,12 @@ const statusRoute = createRoute({
|
|
|
52
53
|
|
|
53
54
|
const modeRoute = createRoute({
|
|
54
55
|
method: 'put',
|
|
55
|
-
path: '/mode',
|
|
56
|
-
summary: 'Change
|
|
56
|
+
path: '/mode/update',
|
|
57
|
+
summary: 'Change mode',
|
|
57
58
|
description:
|
|
58
|
-
'Sets the target security mode. Supported modes: home, away, night, off
|
|
59
|
-
'
|
|
59
|
+
'Sets the target security mode. Supported modes: home, away, night, off. ' +
|
|
60
|
+
'An optional delay (seconds) defers the transition. ' +
|
|
61
|
+
'To trigger the alarm use the POST /mode/trip endpoint instead.',
|
|
60
62
|
security: [{ BearerAuth: [] }],
|
|
61
63
|
request: {
|
|
62
64
|
body: {
|
|
@@ -79,10 +81,40 @@ const modeRoute = createRoute({
|
|
|
79
81
|
},
|
|
80
82
|
});
|
|
81
83
|
|
|
84
|
+
const tripModeRoute = createRoute({
|
|
85
|
+
method: 'post',
|
|
86
|
+
path: '/mode/trip',
|
|
87
|
+
summary: 'Trip mode',
|
|
88
|
+
description:
|
|
89
|
+
'Activates the alarm ("triggered" state). ' +
|
|
90
|
+
'If mode is specified the alarm only fires when the system is currently in that mode. ' +
|
|
91
|
+
'An optional delay (seconds) defers the activation.',
|
|
92
|
+
security: [{ BearerAuth: [] }],
|
|
93
|
+
request: {
|
|
94
|
+
body: {
|
|
95
|
+
content: {
|
|
96
|
+
'application/json': {
|
|
97
|
+
schema: TripModeRequestSchema,
|
|
98
|
+
example: { mode: 'home', delay: 30 },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
required: true,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
responses: {
|
|
105
|
+
204: { description: 'Trip accepted' },
|
|
106
|
+
...AUTH_RESPONSES,
|
|
107
|
+
409: {
|
|
108
|
+
content: { 'application/json': { schema: ErrorSchema } },
|
|
109
|
+
description: 'Trip rejected by the system',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
82
114
|
const armingLockRoute = createRoute({
|
|
83
115
|
method: 'put',
|
|
84
116
|
path: '/switches/arming-lock',
|
|
85
|
-
summary: 'Update arming lock
|
|
117
|
+
summary: 'Update arming lock',
|
|
86
118
|
description: 'Enables or disables the arming lock for a specific mode or globally.',
|
|
87
119
|
security: [{ BearerAuth: [] }],
|
|
88
120
|
request: {
|
|
@@ -117,11 +149,16 @@ export class ServerService {
|
|
|
117
149
|
private readonly stateHandler: StateHandler,
|
|
118
150
|
private readonly tripHandler: TripHandler,
|
|
119
151
|
private readonly switchHandler: SwitchHandler,
|
|
120
|
-
) {
|
|
121
|
-
|
|
122
|
-
start(): void {
|
|
152
|
+
) {
|
|
123
153
|
this.registerRoutes();
|
|
154
|
+
}
|
|
124
155
|
|
|
156
|
+
/** Exposed for testing — returns the underlying Hono app. */
|
|
157
|
+
get app(): OpenAPIHono {
|
|
158
|
+
return this.application;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
start(): void {
|
|
125
162
|
const server = serve(
|
|
126
163
|
{ fetch: this.application.fetch, port: this.options.serverPort! },
|
|
127
164
|
() => this.log.info(`Server (${this.options.serverPort})`),
|
|
@@ -184,27 +221,52 @@ export class ServerService {
|
|
|
184
221
|
});
|
|
185
222
|
});
|
|
186
223
|
|
|
187
|
-
// PUT /mode
|
|
188
|
-
this.application.use('/mode', auth);
|
|
224
|
+
// PUT /mode/update
|
|
225
|
+
this.application.use('/mode/update', auth);
|
|
189
226
|
this.application.openapi(modeRoute, (c) => {
|
|
190
227
|
const { mode, delay = 0 } = c.req.valid('json');
|
|
191
|
-
|
|
228
|
+
const result = this.stateHandler.updateTargetState(MODE_TO_STATE[mode], OriginType.EXTERNAL, delay);
|
|
192
229
|
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
230
|
+
if (!result.success) {
|
|
231
|
+
return c.json({ reason: result.reason ?? 'Mode change rejected by the system' }, 409);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return c.body(null, 204);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// POST /mode/trip
|
|
238
|
+
this.application.use('/mode/trip', auth);
|
|
239
|
+
this.application.openapi(tripModeRoute, (c) => {
|
|
240
|
+
const { mode, delay } = c.req.valid('json');
|
|
241
|
+
const effectiveDelay = delay ?? 0;
|
|
242
|
+
|
|
243
|
+
if (effectiveDelay > 0) {
|
|
244
|
+
const precheck = this.tripHandler.checkTripConditions(true, OriginType.EXTERNAL);
|
|
245
|
+
if (!precheck.success) {
|
|
246
|
+
return c.json({ reason: precheck.reason ?? 'Trip rejected by the system' }, 409);
|
|
201
247
|
}
|
|
248
|
+
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
if (mode != null) {
|
|
251
|
+
this.tripHandler.triggerIfModeSet(MODE_TO_STATE[mode], true);
|
|
252
|
+
} else {
|
|
253
|
+
this.tripHandler.updateTripSwitch(true, OriginType.EXTERNAL, false);
|
|
254
|
+
}
|
|
255
|
+
}, effectiveDelay * 1000);
|
|
256
|
+
|
|
257
|
+
return c.body(null, 204);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let result: ServiceResult;
|
|
261
|
+
|
|
262
|
+
if (mode != null) {
|
|
263
|
+
result = this.tripHandler.triggerIfModeSet(MODE_TO_STATE[mode], true);
|
|
202
264
|
} else {
|
|
203
|
-
result = this.
|
|
265
|
+
result = this.tripHandler.updateTripSwitch(true, OriginType.EXTERNAL, false);
|
|
204
266
|
}
|
|
205
267
|
|
|
206
268
|
if (!result.success) {
|
|
207
|
-
return c.json({ reason: result.reason ?? '
|
|
269
|
+
return c.json({ reason: result.reason ?? 'Trip rejected by the system' }, 409);
|
|
208
270
|
}
|
|
209
271
|
|
|
210
272
|
return c.body(null, 204);
|
|
@@ -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
|
+
audioProcess: null,
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeOptions(overrides: Partial<SecuritySystemOptions> = {}): SecuritySystemOptions {
|
|
31
|
+
return {
|
|
32
|
+
serverPort: 8080,
|
|
33
|
+
serverApiKey: null,
|
|
34
|
+
...overrides,
|
|
35
|
+
} as unknown as SecuritySystemOptions;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeStateHandler(result: ServiceResult = { success: true }) {
|
|
39
|
+
return {
|
|
40
|
+
updateTargetState: vi.fn().mockReturnValue(result),
|
|
41
|
+
isTripping: vi.fn().mockReturnValue(false),
|
|
42
|
+
setCurrentState: vi.fn(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeTripHandler(result: ServiceResult = { success: true }) {
|
|
47
|
+
return {
|
|
48
|
+
updateTripSwitch: vi.fn().mockReturnValue(result),
|
|
49
|
+
triggerIfModeSet: vi.fn().mockReturnValue(result),
|
|
50
|
+
checkTripConditions: vi.fn().mockReturnValue(result),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeSwitchHandler(result: ServiceResult = { success: true }) {
|
|
55
|
+
return {
|
|
56
|
+
updateArmingLock: vi.fn().mockReturnValue(result),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function makeService(
|
|
61
|
+
stateHandlerResult?: ServiceResult,
|
|
62
|
+
tripHandlerResult?: ServiceResult,
|
|
63
|
+
switchHandlerResult?: ServiceResult,
|
|
64
|
+
stateOverrides?: Partial<SystemState>,
|
|
65
|
+
optionsOverrides?: Partial<SecuritySystemOptions>,
|
|
66
|
+
) {
|
|
67
|
+
const { ServerService } = await import('../services/server-service.js');
|
|
68
|
+
const log = makeLog();
|
|
69
|
+
const state = makeState(stateOverrides);
|
|
70
|
+
const options = makeOptions(optionsOverrides);
|
|
71
|
+
const stateHandler = makeStateHandler(stateHandlerResult);
|
|
72
|
+
const tripHandler = makeTripHandler(tripHandlerResult);
|
|
73
|
+
const switchHandler = makeSwitchHandler(switchHandlerResult);
|
|
74
|
+
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
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
|
+
});
|