2026-05-17 14:48:52 -04:00
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
|
|
|
|
|
globalThis.window = {
|
|
|
|
|
EnhancedSecureCryptoUtils: {
|
|
|
|
|
secureLog: { log() {} }
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
globalThis.CustomEvent = class CustomEvent {
|
|
|
|
|
constructor(type, init) {
|
|
|
|
|
this.type = type;
|
|
|
|
|
this.detail = init?.detail;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
globalThis.document = { dispatchEvent() {} };
|
|
|
|
|
|
|
|
|
|
const { EnhancedSecureWebRTCManager } = await import('../src/network/EnhancedSecureWebRTCManager.js');
|
|
|
|
|
|
|
|
|
|
const realSetTimeout = globalThis.setTimeout;
|
|
|
|
|
const realClearTimeout = globalThis.clearTimeout;
|
|
|
|
|
const realSetInterval = globalThis.setInterval;
|
|
|
|
|
const realClearInterval = globalThis.clearInterval;
|
|
|
|
|
|
|
|
|
|
const timers = [];
|
|
|
|
|
globalThis.setTimeout = (callback, delay) => {
|
|
|
|
|
const timer = { kind: 'timeout', callback, delay, cleared: false };
|
|
|
|
|
timers.push(timer);
|
|
|
|
|
return timer;
|
|
|
|
|
};
|
|
|
|
|
globalThis.clearTimeout = (timer) => {
|
|
|
|
|
if (timer) timer.cleared = true;
|
|
|
|
|
};
|
|
|
|
|
globalThis.setInterval = (callback, delay) => {
|
|
|
|
|
const timer = { kind: 'interval', callback, delay, cleared: false };
|
|
|
|
|
timers.push(timer);
|
|
|
|
|
return timer;
|
|
|
|
|
};
|
|
|
|
|
globalThis.clearInterval = (timer) => {
|
|
|
|
|
if (timer) timer.cleared = true;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Periodic log cleanup is tracked and cleared with the existing timer system.
|
|
|
|
|
{
|
|
|
|
|
const manager = {
|
|
|
|
|
_activeTimers: new Set(),
|
|
|
|
|
_startKeySecurityMonitoring() {},
|
|
|
|
|
_verifyAPIIntegrity() { return true; },
|
|
|
|
|
_startSecurityMonitoring() {},
|
|
|
|
|
_cleanupLogs() {},
|
|
|
|
|
_secureLog() {}
|
|
|
|
|
};
|
|
|
|
|
manager._trackActiveTimer = EnhancedSecureWebRTCManager.prototype._trackActiveTimer;
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype._finalizeSecureInitialization.call(manager);
|
|
|
|
|
assert.equal(manager._activeTimers.size, 1);
|
|
|
|
|
const logTimer = manager._logCleanupInterval;
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype._stopAllTimers.call(manager);
|
|
|
|
|
assert.equal(logTimer.cleared, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Deferred file-transfer retries are tracked, cleared, and cannot re-run after session shutdown.
|
|
|
|
|
{
|
|
|
|
|
let initCalls = 0;
|
|
|
|
|
const manager = {
|
|
|
|
|
_sessionAlive: true,
|
|
|
|
|
_activeTimers: new Set(),
|
|
|
|
|
_fileTransferInitRetryTimers: new Set(),
|
|
|
|
|
fileTransferSystem: null,
|
|
|
|
|
dataChannel: { readyState: 'open' },
|
|
|
|
|
isVerified: false,
|
|
|
|
|
_secureLog() {},
|
|
|
|
|
_trackActiveTimer: EnhancedSecureWebRTCManager.prototype._trackActiveTimer,
|
|
|
|
|
_untrackActiveTimer: EnhancedSecureWebRTCManager.prototype._untrackActiveTimer,
|
|
|
|
|
_scheduleFileTransferInitRetry: EnhancedSecureWebRTCManager.prototype._scheduleFileTransferInitRetry,
|
|
|
|
|
initializeFileTransfer() {
|
|
|
|
|
initCalls += 1;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype.initializeFileTransfer.call(manager);
|
|
|
|
|
const retryTimer = [...manager._fileTransferInitRetryTimers][0];
|
|
|
|
|
manager._sessionAlive = false;
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype._stopAllTimers.call(manager);
|
|
|
|
|
if (!retryTimer.cleared) retryTimer.callback();
|
|
|
|
|
assert.equal(retryTimer.cleared, true);
|
|
|
|
|
assert.equal(manager._fileTransferInitRetryTimers.size, 0);
|
|
|
|
|
assert.equal(initCalls, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Repeated peer-disconnect notifications schedule only one delayed cleanup, and cleanup is cancelled on disconnect.
|
|
|
|
|
{
|
|
|
|
|
let disconnectCalls = 0;
|
|
|
|
|
const manager = {
|
|
|
|
|
_sessionAlive: true,
|
|
|
|
|
_activeTimers: new Set(),
|
|
|
|
|
peerDisconnectNotificationSent: false,
|
|
|
|
|
_peerDisconnectCleanupTimer: null,
|
|
|
|
|
deliverMessageToUI() {},
|
|
|
|
|
onStatusChange() {},
|
|
|
|
|
stopHeartbeat() {},
|
|
|
|
|
onKeyExchange() {},
|
|
|
|
|
onVerificationRequired() {},
|
|
|
|
|
disconnect() { disconnectCalls += 1; },
|
|
|
|
|
_secureLog() {},
|
|
|
|
|
_trackActiveTimer: EnhancedSecureWebRTCManager.prototype._trackActiveTimer,
|
|
|
|
|
_untrackActiveTimer: EnhancedSecureWebRTCManager.prototype._untrackActiveTimer
|
|
|
|
|
};
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype.handlePeerDisconnectNotification.call(manager, { reason: 'connection_lost' });
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype.handlePeerDisconnectNotification.call(manager, { reason: 'connection_lost' });
|
|
|
|
|
const scheduled = timers.filter(timer => timer.kind === 'timeout' && timer.delay === 2000);
|
|
|
|
|
assert.equal(scheduled.length, 1);
|
|
|
|
|
|
|
|
|
|
manager._sessionAlive = false;
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype._stopAllTimers.call(manager);
|
|
|
|
|
if (!scheduled[0].cleared) scheduled[0].callback();
|
|
|
|
|
assert.equal(scheduled[0].cleared, true);
|
|
|
|
|
assert.equal(disconnectCalls, 0);
|
|
|
|
|
}
|
2026-05-17 23:16:14 -04:00
|
|
|
|
|
|
|
|
// Intentional disconnect performs notification before teardown without leaving a delayed retry timer behind.
|
|
|
|
|
{
|
|
|
|
|
let notifications = 0;
|
|
|
|
|
const manager = {
|
|
|
|
|
_sessionAlive: true,
|
|
|
|
|
_activeTimers: new Set(),
|
|
|
|
|
intentionalDisconnect: false,
|
|
|
|
|
fileTransferSystem: null,
|
|
|
|
|
dataChannel: null,
|
|
|
|
|
heartbeatChannel: null,
|
|
|
|
|
peerConnection: null,
|
|
|
|
|
decoyTimers: new Map(),
|
|
|
|
|
decoyChannels: new Map(),
|
|
|
|
|
packetBuffer: new Map(),
|
|
|
|
|
chunkQueue: [],
|
|
|
|
|
processedMessageIds: new Set(),
|
|
|
|
|
messageCounter: 0,
|
|
|
|
|
keyVersions: new Map(),
|
|
|
|
|
oldKeys: new Map(),
|
|
|
|
|
currentKeyVersion: 0,
|
|
|
|
|
lastKeyRotation: 0,
|
|
|
|
|
sequenceNumber: 0,
|
|
|
|
|
expectedSequenceNumber: 0,
|
|
|
|
|
replayWindow: new Set(),
|
|
|
|
|
messageQueue: [],
|
|
|
|
|
_heartbeatConfig: {},
|
|
|
|
|
_secureLog() {},
|
|
|
|
|
_stopAllTimers: EnhancedSecureWebRTCManager.prototype._stopAllTimers,
|
|
|
|
|
stopHeartbeat() {},
|
|
|
|
|
stopFakeTrafficGeneration() {},
|
|
|
|
|
_wipeEphemeralKeys() {},
|
|
|
|
|
_hardWipeOldKeys() {},
|
|
|
|
|
_secureCleanupCryptographicMaterials() {},
|
|
|
|
|
_clearVerificationStates() {},
|
|
|
|
|
_secureWipeMemory() {},
|
|
|
|
|
_forceGarbageCollection() { return Promise.resolve(); },
|
|
|
|
|
sendDisconnectNotification() { notifications += 1; },
|
|
|
|
|
onStatusChange() {},
|
|
|
|
|
onKeyExchange() {},
|
|
|
|
|
onVerificationRequired() {}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const timersBeforeDisconnect = timers.length;
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype.disconnect.call(manager);
|
|
|
|
|
assert.equal(notifications, 1);
|
|
|
|
|
assert.equal(manager._activeTimers.size, 0);
|
|
|
|
|
assert.equal(timers.length, timersBeforeDisconnect);
|
|
|
|
|
|
|
|
|
|
// A second disconnect/reconnect-style cycle still does not accumulate deferred timers.
|
|
|
|
|
manager._sessionAlive = true;
|
|
|
|
|
EnhancedSecureWebRTCManager.prototype.disconnect.call(manager);
|
|
|
|
|
assert.equal(notifications, 2);
|
|
|
|
|
assert.equal(manager._activeTimers.size, 0);
|
|
|
|
|
assert.equal(timers.length, timersBeforeDisconnect);
|
|
|
|
|
}
|
2026-05-17 14:48:52 -04:00
|
|
|
} finally {
|
|
|
|
|
globalThis.setTimeout = realSetTimeout;
|
|
|
|
|
globalThis.clearTimeout = realClearTimeout;
|
|
|
|
|
globalThis.setInterval = realSetInterval;
|
|
|
|
|
globalThis.clearInterval = realClearInterval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('Timer lifecycle tests passed');
|