mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
139 lines
6.6 KiB
JavaScript
139 lines
6.6 KiB
JavaScript
// Ревью R3-2: houseplan-space-card подписывала URL подложки и выбрасывала
|
|
// результат — getCardSize() правил временную модель, а render() строил свою
|
|
// заново из конфига, поэтому <image> запрашивал сырой requires_auth-путь и на
|
|
// каждом рендере получал 401. Проверяем весь контракт подписи для этой карточки.
|
|
import { launch, checkAll, finish } from './serve.mjs';
|
|
const { page, browser } = await launch({ width: 900, height: 900 }, 1);
|
|
await page.route('**/api/houseplan/content/plans/_/*.tok.svg?authSig=*', (route) => route.fulfill({
|
|
status: 200,
|
|
contentType: 'image/svg+xml',
|
|
body: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><rect width="16" height="16" fill="#789"/></svg>',
|
|
}));
|
|
const res = await page.evaluate(async () => {
|
|
const out = {};
|
|
await customElements.whenDefined('houseplan-space-card');
|
|
const main = window.__card;
|
|
const raw = '/api/houseplan/content/plans/_/f1.tok.svg';
|
|
const baselineRaw = '/api/houseplan/content/plans/_/f1-baseline.tok.svg';
|
|
|
|
// подложка на защищённом эндпоинте + управляемый ответ на подпись
|
|
const baselineCfg = JSON.parse(JSON.stringify(main._serverCfg));
|
|
baselineCfg.spaces = baselineCfg.spaces.map((s) => (
|
|
s.id === 'f1' ? { ...s, plan_url: baselineRaw } : s
|
|
));
|
|
const candidateCfg = JSON.parse(JSON.stringify(baselineCfg));
|
|
candidateCfg.spaces = candidateCfg.spaces.map((s) => (
|
|
s.id === 'f1' ? { ...s, plan_url: raw } : s
|
|
));
|
|
let servedCfg = baselineCfg;
|
|
let signCalls = 0;
|
|
let failCandidateOnce = true;
|
|
const requestedHrefs = [];
|
|
// A separately mounted test card gets its own HA connection authority. In
|
|
// production cards sharing one connection deliberately share config/signing
|
|
// caches, which is not the behaviour this isolated failure probe exercises.
|
|
const hass = {
|
|
...main.hass,
|
|
connection: Object.create(main.hass.connection),
|
|
callWS: async (m) => {
|
|
if (m.type === 'houseplan/config/get') return { config: servedCfg, rev: signCalls + 1 };
|
|
if (m.type === 'houseplan/layout/get') return { layout: {} };
|
|
if (m.type === 'houseplan/content/sign') {
|
|
signCalls++;
|
|
if (failCandidateOnce && m.paths.includes(raw)) {
|
|
failCandidateOnce = false;
|
|
throw new Error('ws down');
|
|
}
|
|
const urls = {};
|
|
for (const p of m.paths) urls[p] = p + '?authSig=SIG' + signCalls;
|
|
return { urls };
|
|
}
|
|
return { ok: true };
|
|
},
|
|
};
|
|
|
|
const host = document.createElement('div');
|
|
document.body.appendChild(host);
|
|
const card = document.createElement('houseplan-space-card');
|
|
card.setConfig({ type: 'custom:houseplan-space-card', space: 'f1' });
|
|
card.hass = hass;
|
|
host.appendChild(card);
|
|
// connectedCallback may synchronously adopt the module-level warm snapshot;
|
|
// force this isolated probe's server candidate after that warm paint.
|
|
await new Promise((r) => setTimeout(r, 80));
|
|
await card._load(true);
|
|
|
|
const stage = async () => {
|
|
const t0 = Date.now();
|
|
while (!card.renderRoot?.querySelector('.hp-static-stage') && Date.now() - t0 < 6000) {
|
|
await new Promise((r) => setTimeout(r, 60));
|
|
}
|
|
await card.updateComplete;
|
|
return card.renderRoot.querySelector('.hp-static-stage svg image');
|
|
};
|
|
const href = async () => { const im = await stage(); return im ? im.getAttribute('href') : null; };
|
|
|
|
// Establish a decoded protected baseline first. A failed replacement must
|
|
// retain this exact URL, not an unrelated public image from the warm cache.
|
|
await stage();
|
|
const baselineHref = await href();
|
|
servedCfg = candidateCfg;
|
|
await card._load(true);
|
|
|
|
// 1) первая подпись упала → сырой URL в DOM не попадает (иначе 401)
|
|
await stage();
|
|
await new Promise((r) => setTimeout(r, 120));
|
|
out.hrefAfterFailedSign = await href();
|
|
out.failedSignKeptProtectedBaseline = baselineHref?.includes('f1-baseline.tok.svg?authSig=SIG1')
|
|
&& out.hrefAfterFailedSign === baselineHref;
|
|
|
|
// 2) сразу повтора нет: после ошибки подпись уходит в backoff (ревью R4-2),
|
|
// иначе нестабильный сокет получал бы по запросу на каждый рендер
|
|
for (let i = 0; i < 5; i++) { card.requestUpdate(); await card.updateComplete; }
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
out.noRetryStorm = signCalls === 2;
|
|
|
|
// 3) после выдержки повтор проходит
|
|
await new Promise((r) => setTimeout(r, 2100));
|
|
card.requestUpdate(); await card.updateComplete;
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
out.hrefAfterRetry = await href();
|
|
out.retried = signCalls === 3;
|
|
|
|
// 4) повторный рендер не теряет подпись и не просит её заново
|
|
const before = signCalls;
|
|
card.requestUpdate(); await card.updateComplete;
|
|
out.hrefStable = await href();
|
|
out.noExtraSignOnRerender = signCalls === before;
|
|
|
|
// 5) протухшая подпись не отдаётся, стареющая — отдаётся, пока едет замена
|
|
const ent = card._signer.shared.signed;
|
|
ent[raw] = { url: raw + '?authSig=OLD', at: Date.now() - 25 * 3600 * 1000, loaded: true };
|
|
card.requestUpdate(); await card.updateComplete;
|
|
out.hrefWhenExpired = await href();
|
|
ent[raw] = { url: raw + '?authSig=AGING', at: Date.now() - 20 * 3600 * 1000, loaded: true };
|
|
card.requestUpdate(); await card.updateComplete;
|
|
out.hrefWhenAging = await href();
|
|
|
|
// ни один сырой (неподписанный) путь не должен уходить в сеть
|
|
for (const im of card.renderRoot.querySelectorAll('image')) requestedHrefs.push(im.getAttribute('href'));
|
|
out.noRawHrefEver = !requestedHrefs.includes(raw);
|
|
return out;
|
|
});
|
|
// зафиксировано прогоном на v1.45.1 и сверено с кодом
|
|
checkAll(res, {
|
|
// #73 keeps the last complete frame until the replacement is signed and
|
|
// decoded, so a transient signing failure must not blank the background.
|
|
hrefAfterFailedSign: '/api/houseplan/content/plans/_/f1-baseline.tok.svg?authSig=SIG1',
|
|
failedSignKeptProtectedBaseline: true,
|
|
noRetryStorm: true,
|
|
hrefAfterRetry: '/api/houseplan/content/plans/_/f1.tok.svg?authSig=SIG3',
|
|
retried: true,
|
|
hrefStable: '/api/houseplan/content/plans/_/f1.tok.svg?authSig=SIG3',
|
|
noExtraSignOnRerender: true,
|
|
hrefWhenExpired: null,
|
|
hrefWhenAging: '/api/houseplan/content/plans/_/f1.tok.svg?authSig=AGING',
|
|
noRawHrefEver: true,
|
|
});
|
|
await finish(browser);
|