registry: SecurityPanelController with Arm.Response and its errors
Alexa.SecurityPanelController is described from its page: version 3 (the stub of 1.5.2 said 1), armState and the four alarms, Arm and Disarm. ctx.respond() answers Arm with Arm.Response under the namespace of the interface, its payload checked (exitDelayInSeconds 0 to 255, bypassedEndpoints), and Disarm with a Response. A Disarm carries the PIN the user said when the panel is declared with supportedAuthorizationTypes; the handler checks it and answers UNAUTHORIZED, which goes under Alexa.SecurityPanelController like the five other error types. A panel declared without options lists armState, where 1.5.2 listed no property; alarms are declared. 17 examples of the two pages are saved as fixtures. 215 tests pass, 207 before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
f555a5df70
commit
c59cf3ff10
39 changed files with 1248 additions and 23 deletions
|
|
@ -4,8 +4,8 @@
|
|||
const { test } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const {
|
||||
Alex2MQTT, AlexaErrors, LockController, MemoryPublisher, PowerLevelController, ThermostatController,
|
||||
ThermostatControllerSchedule, registry,
|
||||
Alex2MQTT, AlexaErrors, LockController, MemoryPublisher, PowerLevelController, SecurityPanelController,
|
||||
ThermostatController, ThermostatControllerSchedule, registry,
|
||||
} = require("alex2node");
|
||||
const interfaces = require("../fixtures/interfaces.js");
|
||||
const { doc, examples } = require("../helpers/fixtures.js");
|
||||
|
|
@ -33,15 +33,20 @@ for (const { namespace, page, declared, refused } of interfaces.filter((entry) =
|
|||
test(`${namespace}: the directives of ${page}.html reach the handler, a payload that does not fit is answered with INVALID_VALUE`, async () => {
|
||||
const { device, receive, answers } = bridgeWithout();
|
||||
const seen = [];
|
||||
device.add(registry.get(namespace), declared[0].options).on("*", (ctx) => {
|
||||
const descriptor = registry.get(namespace);
|
||||
device.add(descriptor, declared[0].options).on("*", (ctx) => {
|
||||
seen.push([ctx.name, ctx.payload]);
|
||||
return ctx.respond();
|
||||
});
|
||||
|
||||
const printed = examples(page, ".directive").map((name) => doc(page, name).directive);
|
||||
const printed = examples(page, ".directive").map((name) => doc(page, name).directive)
|
||||
.filter((example) => example.header.namespace === namespace);
|
||||
for (const example of printed) await receive(example);
|
||||
assert.deepEqual(seen, printed.map(({ header, payload }) => [header.name, payload]));
|
||||
assert.deepEqual(answers(), printed.map(() => ["alexaResponce", "Alexa", "Response", {}]));
|
||||
assert.deepEqual(answers(), printed.map(({ header }) => {
|
||||
const own = descriptor.responseFor && descriptor.responseFor(header.name);
|
||||
return ["alexaResponce", own ? own.namespace : "Alexa", own ? own.name : "Response", {}];
|
||||
}));
|
||||
|
||||
for (const [name, payload] of refused) await receive(directive(namespace, name, "device-1", "ct-1", payload));
|
||||
assert.deepEqual(
|
||||
|
|
@ -117,3 +122,62 @@ test("Alexa.ThermostatController: an error of the thermostat goes under its name
|
|||
message: "Alexa.ThermostatController.Schedule as device-1 declares it does not take SetAdaptiveRecovery",
|
||||
}]);
|
||||
});
|
||||
|
||||
test("Alexa.SecurityPanelController: Arm is answered with Arm.Response, Disarm with a Response when the PIN is the one of the panel", async () => {
|
||||
const { device, sent, receive, answers } = bridgeWithout();
|
||||
const page = "alexa-securitypanelcontroller";
|
||||
const errors = "alexa-securitypanelcontroller-errorresponse";
|
||||
const open = doc(errors, "BYPASS_NEEDED.error").event.payload;
|
||||
const bypassed = doc(page, "Arm-bypass.response").event.payload;
|
||||
let armState = "DISARMED";
|
||||
const panel = device.add(SecurityPanelController, { supportedAuthorizationTypes: ["FOUR_DIGIT_PIN"], alarms: ["burglaryAlarm"] });
|
||||
device.state((s) => s.set(panel, "armState", armState).set(panel, "burglaryAlarm", { value: "OK" }));
|
||||
panel.on("Arm", (ctx) => {
|
||||
if (armState === "ARMED_AWAY" && ctx.payload.armState !== "ARMED_AWAY") {
|
||||
throw AlexaErrors.of("AUTHORIZATION_REQUIRED", "You must disarm the system before you can set the requested arm state.");
|
||||
}
|
||||
if (!ctx.payload.bypassType) throw AlexaErrors.bypassNeeded(open.message, open.endpointsNeedingBypass);
|
||||
armState = ctx.payload.armState;
|
||||
return ctx.respond(undefined, { payload: bypassed });
|
||||
});
|
||||
panel.on("Disarm", (ctx) => {
|
||||
const { authorization } = ctx.payload;
|
||||
if (!authorization || authorization.value !== "1234") throw AlexaErrors.of("UNAUTHORIZED", "The PIN code is not correct.");
|
||||
armState = "DISARMED";
|
||||
return ctx.respond();
|
||||
});
|
||||
|
||||
await receive(doc(page, "Arm.directive").directive);
|
||||
await receive(doc(page, "Arm-bypass.directive").directive);
|
||||
await receive(directive(panel.namespace, "Arm", "device-1", "ct", { armState: "ARMED_NIGHT" }));
|
||||
await receive(directive(panel.namespace, "Disarm", "device-1", "ct", { authorization: { type: "FOUR_DIGIT_PIN", value: "4321" } }));
|
||||
await receive(directive(panel.namespace, "Disarm", "device-1", "ct", {}));
|
||||
await receive(doc(page, "Disarm.directive").directive);
|
||||
|
||||
const refused = (type) => {
|
||||
const { header, payload } = doc(errors, `${type}.error`).event;
|
||||
return ["alexaResponce", header.namespace, header.name, payload];
|
||||
};
|
||||
assert.deepEqual(answers(), [
|
||||
refused("BYPASS_NEEDED"),
|
||||
["alexaResponce", "Alexa.SecurityPanelController", "Arm.Response", bypassed],
|
||||
refused("AUTHORIZATION_REQUIRED"),
|
||||
refused("UNAUTHORIZED"),
|
||||
refused("UNAUTHORIZED"),
|
||||
["alexaResponce", "Alexa", "Response", {}],
|
||||
]);
|
||||
assert.equal(refused("UNAUTHORIZED")[1], "Alexa.SecurityPanelController");
|
||||
const states = sent.published.filter(({ message }) => message.context)
|
||||
.map(({ message }) => message.context.properties.find((property) => property.name === "armState").value);
|
||||
assert.deepEqual(states, ["ARMED_AWAY", "DISARMED"]);
|
||||
});
|
||||
|
||||
test("Alexa.SecurityPanelController: a payload that is no Arm.Response is not sent, the directive is answered with INTERNAL_ERROR", async () => {
|
||||
const { device, receive, answers } = bridgeWithout();
|
||||
device.add(SecurityPanelController).on("Arm", (ctx) => ctx.respond(undefined, { payload: { exitDelayInSeconds: 300 } }));
|
||||
await receive(doc("alexa-securitypanelcontroller", "Arm.directive").directive);
|
||||
assert.deepEqual(answers(), [["alexaResponce", "Alexa", "ErrorResponse", {
|
||||
type: "INTERNAL_ERROR",
|
||||
message: "payload.exitDelayInSeconds: expected an integer from 0 to 255, got 300",
|
||||
}]]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue