119 lines
6.5 KiB
JavaScript
119 lines
6.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import vm from "node:vm";
|
|
import { buildOrderLines, catalog } from "../stripe.mjs";
|
|
import { consumerFields, licenseCoverage, normalizeSystemConfiguration, publicWizardCatalog, recommendLicenses, systemDeviceTypes } from "../portal-domain.mjs";
|
|
import { buildSymconPackage } from "../symcon-package.mjs";
|
|
|
|
const configuration = () => ({
|
|
name: "Testsystem", location: "Teststandort", devices: {
|
|
buffer_storage: { quantity: 0 },
|
|
heat_pump: { quantity: 1, items: [{ name: "WP", properties: {
|
|
Kontaktart: 1, Kontakt1VariableID: 10001, Kontakt2VariableID: 10002,
|
|
Nennleistung: 4200, Rueckmeldungsart: 1, IstleistungVariableID: 10003,
|
|
Kontakt2Invertiert: true, Mindestlaufzeit: 1500
|
|
} }] }
|
|
}
|
|
});
|
|
|
|
test("heat-pump order includes separate licenses and setup at existing prices", () => {
|
|
const result = buildOrderLines("manager_standard", { heat_pump: 2 }, { modules: { heat_pump: 2 } }, "manager_standard");
|
|
assert.equal(result.length, 2);
|
|
assert.deepEqual(result.map(line => [line.catalogKey, line.lineType, line.quantity, line.unitAmount, line.entitlement]), [
|
|
["heat_pump", "license", 2, 2095, true], ["heat_pump", "setup", 2, 2725, false]
|
|
]);
|
|
});
|
|
|
|
test("heat-pump checkout respects catalog availability and quantities", () => {
|
|
const disabled = { ...catalog, heat_pump: { ...catalog.heat_pump, available: false } };
|
|
assert.throws(() => buildOrderLines("manager_standard", { heat_pump: 1 }, {}, "manager_standard", disabled), /catalog_item_unavailable/);
|
|
for (const quantity of [-1, 0.5, 1000]) {
|
|
assert.throws(() => buildOrderLines("manager_standard", { heat_pump: quantity }), /invalid_quantity/);
|
|
}
|
|
});
|
|
|
|
test("wizard publishes two-contact heat-pump fields without duplicates", () => {
|
|
const fields = publicWizardCatalog().consumerFields.heat_pump;
|
|
assert.equal(new Set(consumerFields.heat_pump.map(field => field.key)).size, consumerFields.heat_pump.length);
|
|
for (const key of ["Kontaktart", "Kontakt1VariableID", "Kontakt2VariableID", "Rueckmeldungsart", "IstleistungVariableID", "MaximaleSperrzeit24h"]) {
|
|
assert.ok(fields.some(field => field.key === key), key);
|
|
}
|
|
assert.equal(systemDeviceTypes.heat_pump.moduleId, "{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}");
|
|
});
|
|
|
|
test("heat-pump settings survive normalize, save and recommendation", () => {
|
|
const result = normalizeSystemConfiguration(configuration());
|
|
assert.deepEqual(normalizeSystemConfiguration(result), result);
|
|
const properties = result.devices.heat_pump.items[0].properties;
|
|
assert.equal(properties.Kontaktart, 1);
|
|
assert.equal(properties.Kontakt1VariableID, 10001);
|
|
assert.equal(properties.Kontakt2VariableID, 10002);
|
|
assert.equal(properties.IstleistungVariableID, 10003);
|
|
assert.equal(properties.Rueckmeldungsart, 1);
|
|
assert.equal(properties.Kontakt2Invertiert, true);
|
|
assert.equal(properties.Mindestlaufzeit, 1500);
|
|
assert.equal(properties.MaximaleSperrzeit, 7200);
|
|
assert.equal(recommendLicenses(result).modules.heat_pump, 1);
|
|
});
|
|
|
|
test("older configurations gain no heat-pump device and retain consumers", () => {
|
|
const input = configuration();
|
|
delete input.devices.heat_pump;
|
|
input.devices.consumer_single = { quantity: 1, items: [{ name: "Bestand", properties: { Nennleistung: 2200 } }] };
|
|
const result = normalizeSystemConfiguration(input);
|
|
assert.deepEqual(result.devices.heat_pump, { quantity: 0, items: [] });
|
|
assert.equal(result.devices.consumer_single.items[0].properties.Nennleistung, 2200);
|
|
});
|
|
|
|
test("legacy coverage reserves single-consumer licenses without double counting", () => {
|
|
assert.equal(licenseCoverage({ heat_pump: 2, consumer_single: 1 }, { consumer_single: 3 }).heat_pump, 2);
|
|
assert.equal(licenseCoverage({ heat_pump: 2, consumer_single: 2 }, { consumer_single: 3 }).heat_pump, 1);
|
|
assert.equal(licenseCoverage({ heat_pump: 2, consumer_single: 3 }, { consumer_single: 3 }).heat_pump, 0);
|
|
assert.equal(licenseCoverage({ heat_pump: 2 }, { heat_pump: 1, consumer_single: 3 }).heat_pump, 2);
|
|
const source = readFileSync(new URL("../public/app.js", import.meta.url), "utf8");
|
|
const functionSource = source.match(/ function licenseCoverage\(requested, owned\) \{[\s\S]*?\n \}/)[0];
|
|
const browserCoverage = vm.runInNewContext(`(${functionSource})`);
|
|
for (let requested = 0; requested < 5; requested++) {
|
|
for (let consumers = 0; consumers < 5; consumers++) {
|
|
const demand = { heat_pump: requested, consumer_single: consumers };
|
|
const owned = { heat_pump: 1, consumer_single: 3 };
|
|
assert.equal(JSON.stringify(browserCoverage(demand, owned)), JSON.stringify(licenseCoverage(demand, owned)));
|
|
}
|
|
}
|
|
});
|
|
|
|
test("package contains heat-pump configuration and preflight before writes", () => {
|
|
const archive = buildSymconPackage(configuration());
|
|
const files = {};
|
|
let offset = 0;
|
|
while (archive.readUInt32LE(offset) === 0x04034b50) {
|
|
const length = archive.readUInt32LE(offset + 18);
|
|
const nameLength = archive.readUInt16LE(offset + 26);
|
|
const extraLength = archive.readUInt16LE(offset + 28);
|
|
const name = archive.subarray(offset + 30, offset + 30 + nameLength).toString();
|
|
const start = offset + 30 + nameLength + extraLength;
|
|
files[name] = archive.subarray(start, start + length).toString();
|
|
offset = start + length;
|
|
}
|
|
const bootstrap = files["bootstrap.php"];
|
|
assert.match(bootstrap, /A31C9274-54F7-4BD2-9804-7AF23E86C4D1/);
|
|
assert.ok(bootstrap.indexOf("// Validate heat-pump bindings") < bootstrap.indexOf("$root = enxCategory"));
|
|
assert.match(bootstrap, /\$contact1 === \$contact2/);
|
|
assert.match(bootstrap, /Typ der Betriebsrueckmeldung/);
|
|
assert.match(bootstrap, /IPS_SetProperty\(\$instance, \(string\) \$property, \$value\)/);
|
|
});
|
|
|
|
test("header has both links, sidebar has neither, and wizard has ten steps", () => {
|
|
const html = readFileSync(new URL("../public/index.html", import.meta.url), "utf8");
|
|
const header = html.match(/<header class="topbar">([\s\S]*?)<\/header>/)[1];
|
|
const sidebar = html.match(/<aside class="sidebar">([\s\S]*?)<\/aside>/)[1];
|
|
assert.match(header, /href="\/docs\/index.html">Dokumentation/);
|
|
assert.match(header, /href="\/docs\/preise.html">Preise/);
|
|
assert.doesNotMatch(sidebar, /Dokumentation|Preise/);
|
|
assert.deepEqual([...html.matchAll(/data-wizard-step="(\d+)"/g)].map(match => Number(match[1])), [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
assert.equal([...html.matchAll(/data-wizard-marker="/g)].length, 10);
|
|
assert.match(html, /data-device-quantity="heat_pump"/);
|
|
assert.match(html, /data-key="heat_pump"/);
|
|
});
|