186 lines
5.1 KiB
JavaScript
186 lines
5.1 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { describe, it } from "node:test";
|
|
import { getMessages } from "../src/i18n.mjs";
|
|
import {
|
|
buildTrackingPayload,
|
|
compareFingerprintSnapshots,
|
|
createFingerprintIdToken,
|
|
createFingerprintShareCard,
|
|
fingerprintIdPreview,
|
|
parseFingerprintIdToken,
|
|
} from "../src/tracking.mjs";
|
|
|
|
function memoryStorage() {
|
|
const values = new Map();
|
|
return {
|
|
getItem: (key) => values.get(key) ?? null,
|
|
setItem: (key, value) => values.set(key, String(value)),
|
|
};
|
|
}
|
|
|
|
const result = {
|
|
score: 62,
|
|
band: "high",
|
|
locale: "zh-CN",
|
|
timezone: "Asia/Shanghai",
|
|
timezoneOffsetMinutes: -480,
|
|
userAgentFamily: "Apple",
|
|
signals: [
|
|
{
|
|
id: "timezone",
|
|
raw: "Asia/Shanghai",
|
|
score: 1,
|
|
contribution: 30,
|
|
weight: 30,
|
|
},
|
|
],
|
|
};
|
|
|
|
describe("tracking payload", () => {
|
|
it("builds stable hashed components without echoing raw fingerprint values", async () => {
|
|
const storage = memoryStorage();
|
|
const first = await buildTrackingPayload(
|
|
{
|
|
result,
|
|
localFacts: [
|
|
{
|
|
id: "canvas-hash",
|
|
value: "secret-canvas-digest",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
storage,
|
|
randomId: () => "return-key-alpha",
|
|
},
|
|
);
|
|
const second = await buildTrackingPayload(
|
|
{
|
|
result,
|
|
localFacts: [
|
|
{
|
|
id: "canvas-hash",
|
|
value: "secret-canvas-digest",
|
|
},
|
|
],
|
|
},
|
|
{ storage },
|
|
);
|
|
|
|
assert.equal(first.visitorKey, "return-key-alpha");
|
|
assert.equal(second.visitorKey, "return-key-alpha");
|
|
assert.equal(first.fingerprintHash, second.fingerprintHash);
|
|
assert.equal(first.components.some((component) => component.id === "fact-canvas-hash"), true);
|
|
assert.doesNotMatch(JSON.stringify(first), /Asia\/Shanghai|secret-canvas-digest/);
|
|
});
|
|
|
|
it("keeps the return key while changing the fingerprint hash when components change", async () => {
|
|
const storage = memoryStorage();
|
|
const first = await buildTrackingPayload(
|
|
{
|
|
result,
|
|
localFacts: [{ id: "canvas-hash", value: "canvas-a" }],
|
|
},
|
|
{
|
|
storage,
|
|
randomId: () => "return-key-beta",
|
|
},
|
|
);
|
|
const second = await buildTrackingPayload(
|
|
{
|
|
result,
|
|
localFacts: [{ id: "canvas-hash", value: "canvas-b" }],
|
|
},
|
|
{ storage },
|
|
);
|
|
|
|
assert.equal(second.visitorKey, first.visitorKey);
|
|
assert.notEqual(second.fingerprintHash, first.fingerprintHash);
|
|
});
|
|
|
|
it("exports and parses a copyable fingerprint ID without raw values", async () => {
|
|
const payload = await buildTrackingPayload(
|
|
{
|
|
result,
|
|
localFacts: [{ id: "canvas-hash", value: "secret-canvas-digest" }],
|
|
},
|
|
{
|
|
storage: memoryStorage(),
|
|
randomId: () => "return-key-copy",
|
|
},
|
|
);
|
|
|
|
const token = createFingerprintIdToken(payload);
|
|
const parsed = parseFingerprintIdToken(token);
|
|
|
|
assert.match(token, /^fp1_/);
|
|
assert.equal(parsed.fingerprintHash, payload.fingerprintHash);
|
|
assert.equal(fingerprintIdPreview(parsed), payload.fingerprintHash.slice(0, 12));
|
|
assert.doesNotMatch(token, /Asia\/Shanghai|secret-canvas-digest/);
|
|
assert.doesNotMatch(JSON.stringify(parsed), /Asia\/Shanghai|secret-canvas-digest/);
|
|
});
|
|
|
|
it("compares a pasted fingerprint ID against the current snapshot", async () => {
|
|
const storage = memoryStorage();
|
|
const previous = await buildTrackingPayload(
|
|
{
|
|
result,
|
|
localFacts: [{ id: "canvas-hash", value: "canvas-a" }],
|
|
},
|
|
{
|
|
storage,
|
|
randomId: () => "return-key-compare",
|
|
},
|
|
);
|
|
const current = await buildTrackingPayload(
|
|
{
|
|
result,
|
|
localFacts: [{ id: "canvas-hash", value: "canvas-b" }],
|
|
},
|
|
{ storage },
|
|
);
|
|
|
|
const comparison = compareFingerprintSnapshots(
|
|
current,
|
|
parseFingerprintIdToken(createFingerprintIdToken(previous)),
|
|
);
|
|
|
|
assert.equal(comparison.exactMatch, false);
|
|
assert.equal(comparison.changedIds.includes("fact-canvas-hash"), true);
|
|
assert.equal(comparison.stableComponents > 0, true);
|
|
assert.equal(comparison.similarity < 1, true);
|
|
});
|
|
|
|
it("creates a share card that is useful without exposing the full token", async () => {
|
|
const payload = await buildTrackingPayload(
|
|
{
|
|
result,
|
|
localFacts: [{ id: "canvas-hash", value: "canvas-a" }],
|
|
},
|
|
{
|
|
storage: memoryStorage(),
|
|
randomId: () => "return-key-card",
|
|
},
|
|
);
|
|
const token = createFingerprintIdToken(payload);
|
|
const card = createFingerprintShareCard({
|
|
copy: getMessages("zh"),
|
|
token,
|
|
comparison: {
|
|
exactMatch: true,
|
|
similarity: 1,
|
|
changedComponents: 0,
|
|
stableComponents: payload.components.length,
|
|
componentCount: payload.components.length,
|
|
verdict: "exact_repeat",
|
|
},
|
|
url: "https://example.com",
|
|
});
|
|
|
|
assert.match(card, /我的浏览器指纹/);
|
|
assert.match(card, new RegExp(fingerprintIdPreview(payload)));
|
|
assert.match(card, /https:\/\/example\.com/);
|
|
assert.doesNotMatch(card, new RegExp(token));
|
|
});
|
|
});
|