src/lib/preview/ — framework-free ESM modules, per design §12–14: - contracts.js: versioned manifest/item/source-ref factories; signed URLs structurally excluded from manifests; Apple identity first-class. - lease.js: discriminated MediaLease union; computeSafeForMs derives lifetimes provider-clock-first with fallback/contradiction handling (margins are ADRs backed by Phase 0 soak evidence). - redact.js: URL redaction (host+hash+exp only) and the HTTPS + exact host allowlist gate applied to initial URLs and every redirect. - manifest.js: artist (rank order, exact-id dedupe), album (disc/track sort, context inheritance) and fixture builders — pure, no fetching. - session.js: reducer with session/generation guards on every async action; keyed active item with derived playable indices/cursor; auto-advance under fresh generations; close is terminal. - resolver.js: PreviewResolver contract + scripted fixture resolver with classified failures (stale_lease/not_found/network/…). - transport.js: transport seam + deterministic fake (manual ticks). - focus.js: revision gateway (all main-player entry points; focus- internal tag) and the coordinator — acquisition only via a generation-bound silence ack, restore only with revisions unchanged. tests/preview/*.test.mjs run via `npm test` (node --test): 61 passing, covering the full Phase 1 list in IMPLEMENTATION-PLAN §6 including the exit gate: stale lease/play events are inert, no restoration after any simulated main command, and no component renders anywhere.
125 lines
5.0 KiB
JavaScript
125 lines
5.0 KiB
JavaScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
computeSafeForMs,
|
|
makeNativeLease,
|
|
makeWebviewLease,
|
|
isValidLease,
|
|
LEASE_FALLBACK_SAFE_MS,
|
|
LEASE_SAFETY_MARGIN_MS,
|
|
LEASE_MAX_SAFE_MS,
|
|
} from "../../src/lib/preview/lease.js";
|
|
import { redactUrl, parseSignedExpiryMs, isAllowedMediaUrl } from "../../src/lib/preview/redact.js";
|
|
|
|
const SRC = Object.freeze({ provider: "deezer", providerTrackId: "1", matchConfidence: "native" });
|
|
const NOW = 1_784_760_000_000;
|
|
const MIN = 60_000;
|
|
|
|
// ---------------------------------------------------------- safeForMs -----
|
|
|
|
test("valid exp + Date: provider-clock lifetime minus margin", () => {
|
|
const safe = computeSafeForMs({ expEpochMs: NOW + 14 * MIN, dateHeaderMs: NOW, localNowMs: NOW });
|
|
assert.equal(safe, 14 * MIN - LEASE_SAFETY_MARGIN_MS);
|
|
});
|
|
|
|
test("local clock AHEAD of provider: the shorter local view wins", () => {
|
|
const safe = computeSafeForMs({
|
|
expEpochMs: NOW + 14 * MIN,
|
|
dateHeaderMs: NOW,
|
|
localNowMs: NOW + 10 * MIN, // local thinks only 4 min remain
|
|
});
|
|
assert.equal(safe, 4 * MIN - LEASE_SAFETY_MARGIN_MS);
|
|
});
|
|
|
|
test("local clock BEHIND provider: local view cannot extend the lease", () => {
|
|
const safe = computeSafeForMs({
|
|
expEpochMs: NOW + 14 * MIN,
|
|
dateHeaderMs: NOW,
|
|
localNowMs: NOW - 60 * MIN, // local view claims 74 min
|
|
});
|
|
assert.equal(safe, 14 * MIN - LEASE_SAFETY_MARGIN_MS);
|
|
});
|
|
|
|
test("missing exp → short capped fallback", () => {
|
|
assert.equal(computeSafeForMs({ expEpochMs: null, dateHeaderMs: NOW, localNowMs: NOW }), LEASE_FALLBACK_SAFE_MS);
|
|
});
|
|
|
|
test("malformed exp → fallback", () => {
|
|
assert.equal(computeSafeForMs({ expEpochMs: NaN, dateHeaderMs: NOW, localNowMs: NOW }), LEASE_FALLBACK_SAFE_MS);
|
|
});
|
|
|
|
test("contradictory: exp before Date → fallback", () => {
|
|
assert.equal(
|
|
computeSafeForMs({ expEpochMs: NOW - MIN, dateHeaderMs: NOW, localNowMs: NOW }),
|
|
LEASE_FALLBACK_SAFE_MS
|
|
);
|
|
});
|
|
|
|
test("contradictory: absurd multi-hour lifetime → fallback", () => {
|
|
assert.equal(
|
|
computeSafeForMs({ expEpochMs: NOW + 6 * 60 * MIN, dateHeaderMs: NOW, localNowMs: NOW }),
|
|
LEASE_FALLBACK_SAFE_MS
|
|
);
|
|
});
|
|
|
|
test("missing Date but sane local view → local view minus margin, capped", () => {
|
|
const safe = computeSafeForMs({ expEpochMs: NOW + 14 * MIN, dateHeaderMs: null, localNowMs: NOW });
|
|
assert.equal(safe, 14 * MIN - LEASE_SAFETY_MARGIN_MS);
|
|
const capped = computeSafeForMs({ expEpochMs: NOW + 90 * MIN, dateHeaderMs: null, localNowMs: NOW });
|
|
assert.equal(capped, LEASE_MAX_SAFE_MS - LEASE_SAFETY_MARGIN_MS);
|
|
});
|
|
|
|
test("already expired with no Date → fallback (refresh immediately-ish, never negative)", () => {
|
|
assert.equal(
|
|
computeSafeForMs({ expEpochMs: NOW - MIN, dateHeaderMs: null, localNowMs: NOW }),
|
|
LEASE_FALLBACK_SAFE_MS
|
|
);
|
|
});
|
|
|
|
// ------------------------------------------------------ union validity ----
|
|
|
|
test("discriminated union: native carries exactly an opaque id", () => {
|
|
const lease = makeNativeLease({ source: SRC, opaqueLeaseId: "op-1", safeForMs: 1000 });
|
|
assert.ok(isValidLease(lease));
|
|
assert.equal(lease.url, undefined);
|
|
assert.ok(!isValidLease({ ...lease, url: "https://x" }));
|
|
});
|
|
|
|
test("discriminated union: webview carries exactly a url", () => {
|
|
const lease = makeWebviewLease({ source: SRC, url: "https://cdnt-preview.dzcdn.net/a.mp3", safeForMs: 1000 });
|
|
assert.ok(isValidLease(lease));
|
|
assert.equal(lease.opaqueLeaseId, undefined);
|
|
assert.ok(!isValidLease({ ...lease, opaqueLeaseId: "x" }));
|
|
});
|
|
|
|
test("constructors refuse the wrong locator", () => {
|
|
assert.throws(() => makeNativeLease({ source: SRC, safeForMs: 1 }));
|
|
assert.throws(() => makeWebviewLease({ source: SRC, safeForMs: 1 }));
|
|
});
|
|
|
|
// ------------------------------------------------------------ redaction ---
|
|
|
|
test("redactUrl yields host + hash + exp, never the url", () => {
|
|
const url = "https://cdnt-preview.dzcdn.net/api/x.mp3?hdnea=exp=1784761537~acl=/x*~hmac=deadbeef";
|
|
const r = redactUrl(url);
|
|
assert.equal(r.host, "cdnt-preview.dzcdn.net");
|
|
assert.equal(r.expMs, 1784761537000);
|
|
assert.match(r.hash, /^[0-9a-f]{8}$/);
|
|
assert.ok(!JSON.stringify(r).includes("hmac"));
|
|
});
|
|
|
|
test("parseSignedExpiryMs handles bare exp params and garbage", () => {
|
|
assert.equal(parseSignedExpiryMs("https://h/x?exp=1784761537"), 1784761537000);
|
|
assert.equal(parseSignedExpiryMs("https://h/x"), null);
|
|
assert.equal(parseSignedExpiryMs(null), null);
|
|
});
|
|
|
|
test("host policy: https + exact allowlist, redirects re-checked by the same gate", () => {
|
|
assert.ok(isAllowedMediaUrl("https://cdnt-preview.dzcdn.net/a.mp3"));
|
|
assert.ok(!isAllowedMediaUrl("http://cdnt-preview.dzcdn.net/a.mp3")); // scheme
|
|
assert.ok(!isAllowedMediaUrl("https://evil.dzcdn.net/a.mp3")); // host
|
|
assert.ok(!isAllowedMediaUrl("https://cdnt-preview.dzcdn.net.evil.com/a.mp3")); // suffix trick
|
|
assert.ok(!isAllowedMediaUrl("not a url"));
|
|
});
|