Files
bitvid/tests/nostr/dmDecryptWorkerClient.test.mjs
T
google-labs-jules[bot]andPR0M3TH3AN 072be49068 test: add tests for dmDecryptWorkerClient
- Create `tests/nostr/dmDecryptWorkerClient.test.mjs` to test `decryptDmInWorker`.
- Implement `MockWorker` to simulate worker behavior in `node:test`.
- Cover scenarios: success, error response, error event, timeout, invalid input, and worker availability.
- Use dynamic imports with cache busting to reset singleton state between tests.
- Ensure `devLogger` warnings are verified by mocking `console.warn` correctly.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-08 01:17:49 +00:00

256 lines
7.7 KiB
JavaScript

import { describe, it, before, after, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
// Mock Worker globally
class MockWorker {
constructor(scriptURL) {
this.scriptURL = scriptURL;
this.listeners = {};
MockWorker.instances.push(this);
// Mimic the real worker path
assert.match(String(scriptURL), /dmDecryptWorker\.js$/);
}
addEventListener(event, handler) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(handler);
}
removeEventListener(event, handler) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter((h) => h !== handler);
}
}
postMessage(data) {
// Simulate async processing
setImmediate(() => {
if (MockWorker.onPostMessage) {
MockWorker.onPostMessage(this, data);
}
});
}
// Helper to simulate incoming message from worker
emitMessage(data) {
if (this.listeners['message']) {
const event = { data };
this.listeners['message'].forEach((handler) => handler(event));
}
}
// Helper to simulate worker error
emitError(error) {
if (this.listeners['error']) {
this.listeners['error'].forEach((handler) => handler(error));
}
}
terminate() {}
}
MockWorker.instances = [];
MockWorker.onPostMessage = null;
describe('dmDecryptWorkerClient', () => {
let originalWorker;
let originalConsoleWarn;
let originalDevOverride;
before(() => {
originalWorker = globalThis.Worker;
originalConsoleWarn = console.warn;
originalDevOverride = globalThis.__BITVID_DEV_MODE_OVERRIDE__;
// Set dev mode to true to ensure logger calls console.warn
globalThis.__BITVID_DEV_MODE_OVERRIDE__ = true;
});
after(() => {
globalThis.Worker = originalWorker;
console.warn = originalConsoleWarn;
globalThis.__BITVID_DEV_MODE_OVERRIDE__ = originalDevOverride;
});
let warnMock;
let initialCallCount = 0;
beforeEach(() => {
MockWorker.instances = [];
MockWorker.onPostMessage = null;
if (!warnMock) {
warnMock = mock.fn();
console.warn = warnMock;
}
initialCallCount = warnMock.mock.calls.length;
globalThis.Worker = MockWorker;
});
afterEach(() => {
// Cleanup is handled by new import per test or global reset
// We don't need to manually destroy the module as we import a fresh one
});
// Helper to load a fresh module instance
async function loadModule() {
// Cache busting to ensure we get a fresh module with reset internal state (workerInstance = null)
const mod = await import(`../../js/nostr/dmDecryptWorkerClient.js?t=${Date.now()}-${Math.random()}`);
return mod;
}
it('should support worker environment', async () => {
const { isDmDecryptWorkerSupported } = await loadModule();
assert.strictEqual(isDmDecryptWorkerSupported(), true);
});
it('should reject if Worker is unavailable', async () => {
globalThis.Worker = undefined;
const { decryptDmInWorker } = await loadModule();
await assert.rejects(
decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: 'ct' }),
{ message: 'dm-worker-unavailable' }
);
});
it('should reject if inputs are invalid', async () => {
const { decryptDmInWorker } = await loadModule();
await assert.rejects(
decryptDmInWorker({ privateKey: '', targetPubkey: 'tp', ciphertext: 'ct' }),
{ message: 'dm-worker-invalid-input' }
);
await assert.rejects(
decryptDmInWorker({ privateKey: 'pk', targetPubkey: '', ciphertext: 'ct' }),
{ message: 'dm-worker-invalid-input' }
);
await assert.rejects(
decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: '' }),
{ message: 'dm-worker-invalid-input' }
);
});
it('should successfully decrypt message', async () => {
const { decryptDmInWorker, getDmDecryptWorkerQueueSize } = await loadModule();
MockWorker.onPostMessage = (worker, data) => {
assert.strictEqual(data.privateKey, 'pk');
assert.strictEqual(data.ciphertext, 'ct');
assert.strictEqual(data.targetPubkey, 'tp');
worker.emitMessage({
id: data.id,
ok: true,
plaintext: 'decrypted-text'
});
};
const promise = decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: 'ct' });
assert.strictEqual(getDmDecryptWorkerQueueSize(), 1);
const result = await promise;
assert.strictEqual(result, 'decrypted-text');
assert.strictEqual(getDmDecryptWorkerQueueSize(), 0);
});
it('should handle worker error response', async () => {
const { decryptDmInWorker } = await loadModule();
MockWorker.onPostMessage = (worker, data) => {
worker.emitMessage({
id: data.id,
error: { message: 'decryption-failed', name: 'DecryptError' }
});
};
await assert.rejects(
decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: 'ct' }),
(err) => {
assert.strictEqual(err.message, 'decryption-failed');
assert.strictEqual(err.name, 'DecryptError');
return true;
}
);
});
it('should handle worker error event', async () => {
const { decryptDmInWorker } = await loadModule();
MockWorker.onPostMessage = (worker, data) => {
worker.emitError(new Error('worker-crashed'));
};
await assert.rejects(
decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: 'ct' }),
{ message: 'worker-crashed' }
);
// Check that warning was logged
const newCalls = warnMock.mock.calls.slice(initialCallCount);
assert.strictEqual(newCalls.length, 1);
const args = newCalls[0].arguments;
assert.match(args[0], /Worker error/);
});
it('should timeout if worker does not respond', async () => {
const { decryptDmInWorker } = await loadModule();
// Use a short timeout for testing
const timeoutMs = 100;
MockWorker.onPostMessage = (worker, data) => {
// Do nothing, let it timeout
};
await assert.rejects(
decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: 'ct', timeoutMs }),
{ message: 'dm-worker-timeout' }
);
});
it('should initialize worker lazily and reuse instance', async () => {
const mod = await loadModule();
// First call creates worker
MockWorker.onPostMessage = (worker, data) => worker.emitMessage({id: data.id, ok: true, plaintext: 'p1'});
await mod.decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: 'ct' });
assert.strictEqual(MockWorker.instances.length, 1);
const firstInstance = MockWorker.instances[0];
// Second call reuses worker
MockWorker.onPostMessage = (worker, data) => worker.emitMessage({id: data.id, ok: true, plaintext: 'p2'});
await mod.decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: 'ct' });
assert.strictEqual(MockWorker.instances.length, 1);
assert.strictEqual(MockWorker.instances[0], firstInstance);
});
it('should recreate worker if creation fails', async () => {
// This is hard to test because ensureWorker catches the error and sets workerInstance to null
// but only logs it. It returns null.
globalThis.Worker = class BrokenWorker {
constructor() { throw new Error('Init failed'); }
};
const mod = await loadModule();
await assert.rejects(
mod.decryptDmInWorker({ privateKey: 'pk', targetPubkey: 'tp', ciphertext: 'ct' }),
{ message: 'dm-worker-unavailable' }
);
const newCalls = warnMock.mock.calls.slice(initialCallCount);
assert.strictEqual(newCalls.length, 1);
assert.match(newCalls[0].arguments[0], /Failed to create worker/);
});
});