"use strict"; // The registry: every descriptor is well formed, the ones described in full agree with the examples of their page, // and every interface name of 1.x resolves. const { test } = require("node:test"); const assert = require("node:assert/strict"); const { registry, AlexaInterface, AlexaInterfaceType, DeclarationError, SchemaError } = require("alex2node"); const interfaces = require("../fixtures/interfaces.js"); const { doc, examples, capabilities, reported } = require("../helpers/fixtures.js"); const described = registry.list().filter((descriptor) => descriptor.tier !== 3); const stubs = registry.list().filter((descriptor) => descriptor.tier === 3); test("every descriptor has a namespace, a version, a kind and the page it was written from", () => { const namespaces = registry.list().map((descriptor) => descriptor.namespace); assert.deepEqual(namespaces, [...namespaces].sort(), "list() is ordered by namespace"); for (const descriptor of registry.list()) { const { namespace } = descriptor; assert.match(namespace, /^Alexa(\.[A-Za-z]+)*$/); assert.match(descriptor.version, /^\d+(\.\d+)?$/, namespace); assert.match(descriptor.doc, /^https:\/\/developer\.amazon\.com\/[\w\-./]+\.html$/, namespace); assert.ok(["base", "controller", "sensor", "eventSource"].includes(descriptor.kind), namespace); assert.ok([1, 2, 3].includes(descriptor.tier), namespace); assert.equal(typeof descriptor.instanced, "boolean", namespace); for (const [key, property] of Object.entries(descriptor.properties)) assert.equal(property.name, key, namespace); for (const [key, directive] of Object.entries(descriptor.directives)) assert.equal(directive.name, key, namespace); } }); test("a descriptor of tier 1 or 2 has an entry in test/fixtures/interfaces.js, and every entry a descriptor", () => { assert.deepEqual(described.map((descriptor) => descriptor.namespace), interfaces.map((entry) => entry.namespace).sort()); }); for (const { namespace, page, declared, unfit = [] } of interfaces) { test(`${namespace}: version, properties, directive payloads and property values of ${page}.html`, () => { const descriptor = registry.get(namespace); assert.ok(descriptor.doc.endsWith(`/${page}.html`), descriptor.doc); // The discovery examples of the page, and of another page when the entry declares from one const discovery = examples(page).filter((example) => example.startsWith("discovery")).map((example) => [page, example]); for (const { page: other = page, example } of declared) discovery.push([other, example]); for (const [from, example] of discovery) { for (const capability of capabilities(from, namespace, example)) { assert.equal(capability.version, descriptor.version, `${example}: version`); assert.equal("instance" in capability, descriptor.instanced, `${example}: instance`); const supported = capability.properties ? capability.properties.supported.map((property) => property.name) : []; assert.deepEqual(supported, Object.keys(descriptor.properties), `${example}: properties`); } } const directives = examples(page, ".directive").map((name) => doc(page, name).directive); assert.deepEqual( directives.map((directive) => directive.header.name).sort(), Object.keys(descriptor.directives).filter((name) => name !== "ReportState").sort(), "the page has an example of every directive" ); for (const { header, payload } of directives) { assert.equal(header.namespace, namespace); assert.deepEqual(descriptor.directives[header.name].payload.parse(payload, "payload"), payload); } const values = reported(page, namespace); for (const { example, property } of values) { const known = descriptor.properties[property.name]; assert.ok(known, `${example}: ${property.name} is not a property of the descriptor`); assert.deepEqual(known.value.parse(property.value, property.name), property.value, example); } if (Object.keys(descriptor.properties).length > 0) assert.ok(values.length > 0, "the page reports no property of the interface"); for (const [name, value] of unfit) { assert.throws(() => descriptor.properties[name].value.parse(value, name), SchemaError, `${name} ${JSON.stringify(value)}`); } }); } test("the error types of an interface are in the error type table under its error namespace", () => { const { ERROR_TYPES } = require("../../dist/cjs/registry/catalog.js"); const withErrors = registry.list().filter((descriptor) => descriptor.errorNamespace || descriptor.errorTypes); // The generic controllers answer a safety problem under Alexa.Safety for (const name of ["Alexa.ModeController", "Alexa.RangeController", "Alexa.ToggleController"]) { assert.deepEqual(registry.get(name).errorTypes, ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"], name); } for (const { namespace, errorNamespace, errorTypes } of withErrors) { assert.ok(errorTypes.length > 0, namespace); for (const type of errorTypes) assert.equal(ERROR_TYPES[type], errorNamespace, `${namespace}: ${type}`); } }); test("Alexa.EndpointHealth is version 3.1 and reports connectivity, with a reason when it has one", () => { const { version, properties } = registry.get("Alexa.EndpointHealth"); assert.equal(version, "3.1"); assert.deepEqual(Object.keys(properties), ["connectivity"]); const unreachable = doc("alexa-endpointhealth", "connectivity.property").value; assert.deepEqual(properties.connectivity.value.parse(unreachable), { value: "UNREACHABLE", reason: "WIFI_BAD_PASSWORD" }); assert.deepEqual(properties.connectivity.value.parse({ value: "OK" }), { value: "OK" }); assert.throws( () => properties.connectivity.value.parse({ value: "UNREACHABLE", reason: "UNPLUGGED" }, "connectivity"), /^SchemaError: connectivity\.reason: expected WIFI_BAD_PASSWORD \| .* \| UNKNOWN, got "UNPLUGGED"$/ ); }); test("a payload or a value that does not fit is refused with its path", () => { const { directives, properties } = registry.get("Alexa.BrightnessController"); assert.throws( () => directives.SetBrightness.payload.parse({ brightness: "lots" }, "payload"), /^SchemaError: payload\.brightness: expected an integer from 0 to 100, got "lots"$/ ); assert.throws(() => directives.AdjustBrightness.payload.parse({ brightnessDelta: 101 }, "payload"), /from -100 to 100/); assert.throws(() => properties.brightness.value.parse(-1, "brightness"), /^SchemaError: brightness: expected an integer/); assert.throws(() => registry.get("Alexa.PowerController").properties.powerState.value.parse("on", "powerState"), /expected ON \| OFF/); }); test("registry.get: the namespace and the AlexaInterfaceType member give the same descriptor; an unknown name throws", () => { assert.equal(registry.get(AlexaInterfaceType.POWER_CONTROLLER), registry.get("Alexa.PowerController")); assert.equal(registry.has("Alexa.PowerController"), true); assert.equal(registry.has("Alexa.TeleportController"), false); assert.throws( () => registry.get("Alexa.TeleportController"), (err) => err instanceof DeclarationError && err.message === '"Alexa.TeleportController" is not an interface alex2node knows' ); assert.throws(() => registry.get(AlexaInterfaceType.UNKNOWN), DeclarationError); }); test("every AlexaInterfaceType is in the registry: described, or a stub with the version and properties of 1.5.2", () => { const names = Object.values(AlexaInterfaceType).filter((name) => name !== AlexaInterfaceType.UNKNOWN); // The 69 of 1.x and HUMIDITY_SENSOR assert.equal(names.length, 70); assert.deepEqual(names.filter((name) => !registry.has(name)), []); // "Alexa" is the one interface that was never in the enum assert.deepEqual(registry.list().map((descriptor) => descriptor.namespace).filter((name) => !names.includes(name)), ["Alexa"]); for (const stub of stubs) { assert.deepEqual(stub.directives, {}, stub.namespace); assert.equal(stub.instanced, false, stub.namespace); } const thermostat = registry.get("Alexa.ThermostatController"); assert.equal(thermostat.version, "3.2"); assert.deepEqual(Object.keys(thermostat.properties), ["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"]); }); test("AlexaInterface.getVersion() and getProps() read the registry", () => { for (const descriptor of registry.list().filter(({ namespace }) => namespace !== "Alexa")) { const capability = new AlexaInterface(descriptor.namespace); assert.equal(capability.getVersion(), descriptor.version, descriptor.namespace); assert.deepEqual(capability.getProps(), Object.keys(descriptor.properties), descriptor.namespace); } // 1.5.2 had no version for these two and put "UNKNOWN" in discovery assert.equal(new AlexaInterface(AlexaInterfaceType.TIME_HOLD_CONTROLLER).getVersion(), "3"); assert.equal(new AlexaInterface(AlexaInterfaceType.CAMERA_LIVE_VIEW_CONTROLLER).getVersion(), "1.7"); });