Import the 2026-10-09 source release, Fastify/SQLite backend, restored source assets, and analytics-enabled server build. Replace files from the previous release while retaining the existing repository history. Make builds portable across Windows and Linux, configure local startup, normalize Windows static asset paths, and fix test fixture cleanup and file URL handling. Exclude local environment settings, databases, dependencies, validation output, and TypeScript build cache from version control. Validation: npm ci, static and server builds, 106 arrival tests, and actual local HTTP checks passed. The full suite passed 467 of 470 tests; three tests still require QA fixtures omitted from the supplied release, documented in README-서버실행.md.
88 lines
4.8 KiB
JavaScript
88 lines
4.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { access } from 'node:fs/promises';
|
|
import { createHash, webcrypto } from 'node:crypto';
|
|
import { test } from 'node:test';
|
|
import { loadTypescript } from './support/load-typescript.mjs';
|
|
|
|
const moduleURL = new URL('../src/analytics/Identity.ts', import.meta.url);
|
|
assert.equal(await access(moduleURL).then(() => true, () => false), true, 'private browser identity module must exist');
|
|
const { getVisitorId, makeDogId, VISITOR_ID_STORAGE_KEY } = await loadTypescript(moduleURL);
|
|
const first = '01234567-89ab-4cde-8f01-23456789abcd';
|
|
const second = '12345678-9abc-4def-9012-3456789abcde';
|
|
const digest = webcrypto.subtle.digest.bind(webcrypto.subtle);
|
|
const hash = (visitor, name) => makeDogId(visitor, name, digest);
|
|
|
|
test('visitor UUID persists across collector reloads without creating another identifier', () => {
|
|
const values = new Map();
|
|
const storage = { getItem: key => values.get(key) ?? null, setItem: (key, value) => values.set(key, value) };
|
|
assert.equal(getVisitorId({ getStorage: () => storage, randomUUID: () => first }), first);
|
|
assert.equal(values.get(VISITOR_ID_STORAGE_KEY), first);
|
|
assert.equal(VISITOR_ID_STORAGE_KEY, 'behind-glass.visitor-id.v1');
|
|
assert.equal(getVisitorId({ getStorage: () => storage, randomUUID: () => { throw Error('must reuse stored ID'); } }), first);
|
|
});
|
|
|
|
test('malformed stored UUID is replaced by a fresh valid random UUID', () => {
|
|
for (const bad of ['', 'not-a-uuid', first + 'x', '00000000-0000-0000-0000-000000000000', first.replace('-4cde-', '-1cde-'), first.replace('-4cde-', '-7cde-')]) {
|
|
let value = bad;
|
|
assert.equal(getVisitorId({ getStorage: () => ({ getItem: () => value, setItem: (_key, next) => { value = next; } }), randomUUID: () => first }), first);
|
|
assert.equal(value, first);
|
|
}
|
|
});
|
|
|
|
test('blocked storage getter, read, write, and absent storage use one UUID per collector', () => {
|
|
for (const getStorage of [
|
|
() => undefined,
|
|
() => { throw Error('blocked getter'); },
|
|
() => ({ getItem() { throw Error('blocked read'); }, setItem() { throw Error('blocked write'); } }),
|
|
() => ({ getItem: () => null, setItem() { throw Error('full'); } }),
|
|
]) {
|
|
let created = 0;
|
|
const options = { getStorage, randomUUID: () => (++created === 1 ? first : second) };
|
|
assert.equal(getVisitorId(options), first);
|
|
assert.equal(getVisitorId(options), first);
|
|
assert.equal(created, 1);
|
|
assert.equal(getVisitorId({ getStorage, randomUUID: () => second }), second);
|
|
}
|
|
});
|
|
|
|
test('unavailable, throwing, or invalid random UUID creation leaves telemetry inert', () => {
|
|
for (const randomUUID of [undefined, () => { throw Error('unavailable'); }, () => 'invented-fingerprint', () => first.replace('-4cde-', '-1cde-'), () => first.replace('-4cde-', '-7cde-')]) {
|
|
const options = { getStorage: () => undefined, randomUUID };
|
|
assert.equal(getVisitorId(options), undefined);
|
|
assert.equal(getVisitorId(options), undefined);
|
|
}
|
|
});
|
|
|
|
test('stored uppercase UUID is replaced and noncanonical generated UUID stays inert', () => {
|
|
let stored = first.toUpperCase();
|
|
const storage = { getItem: () => stored, setItem: (_key, value) => { stored = value; } };
|
|
assert.equal(getVisitorId({ getStorage: () => storage, randomUUID: () => second }), second);
|
|
assert.equal(stored, second);
|
|
assert.equal(getVisitorId({ getStorage: () => undefined, randomUUID: () => first.toUpperCase() }), undefined);
|
|
});
|
|
|
|
test('empty and whitespace names use the explicit default name hash', async () => {
|
|
assert.equal(await hash(first, ''), await hash(first, '멍멍이'));
|
|
assert.equal(await hash(first, ' \n\t '), await hash(first, '멍멍이'));
|
|
});
|
|
|
|
test('hash uses NFC, trim and 20 code points without friendly display suffixes', async () => {
|
|
assert.equal(await hash(first, ' 호롱 '), await hash(first, '호롱'));
|
|
assert.equal(await hash(first, '🐕'.repeat(21)), await hash(first, '🐕'.repeat(20)));
|
|
assert.notEqual(await hash(first, '호롱'), await hash(first, '호롱이'));
|
|
});
|
|
|
|
test('WebCrypto hashes the exact versioned UTF-8 JSON tuple to lowercase SHA-256', async () => {
|
|
const expected = createHash('sha256').update(JSON.stringify([1, first, '호롱']), 'utf8').digest('hex');
|
|
assert.equal(await hash(first, '호롱'), expected);
|
|
assert.match(expected, /^[0-9a-f]{64}$/);
|
|
assert.notEqual(await hash('a|b', 'c'), await hash('a', 'b|c'), 'tuple framing cannot collide through a delimiter');
|
|
});
|
|
|
|
test('missing, throwing, rejected or malformed digest disables only the affected hash', async () => {
|
|
for (const unavailable of [undefined, () => { throw Error('unavailable'); }, async () => { throw Error('failed'); }, async () => new ArrayBuffer(0)]) {
|
|
assert.equal(await makeDogId(first, '호롱', unavailable), '');
|
|
}
|
|
assert.match(await hash(first, '호롱'), /^[0-9a-f]{64}$/);
|
|
});
|