Electron desktop app with a Svelte 5 interface for any Mumble server. - Mumble protocol core: TLS, handshake, channels, users, text, plugin data, client-side pacing of Murmur's rate limits - Voice: WebCodecs Opus over the TCP tunnel, voice activity, push to talk, always-on, devices, per-user volume and local mute - Several servers at once, voice on one; server rail with icons and ordering - Chat: channels, direct messages, side chat, file sharing through f0ckm, inline images without it, click-to-play YouTube - Profiles with rich descriptions, registration, rename, nicknames, connection information and moderation menus - Identity wizard, multiple identities, PKCS#12 import/export, desktop Mumble certificate import, certificate pinning and viewer - Tray icon with voice state - Unit, server and end-to-end tests against a real Murmur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
71 lines
3.4 KiB
TypeScript
71 lines
3.4 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, statSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { generateIdentity, identityFromP12, identityToP12 } from '../electron/identity.ts';
|
|
import { IdentityStore } from '../electron/identity-store.ts';
|
|
|
|
const tmp = () => mkdtempSync(path.join(tmpdir(), 'mumh5-id-'));
|
|
|
|
test('PKCS#12 export and import round trip', () => {
|
|
const id = generateIdentity('roundtrip', 'rt@example.com');
|
|
const p12 = identityToP12(id, 'hunter22', 'roundtrip');
|
|
const back = identityFromP12(p12, 'hunter22');
|
|
assert.equal(back.fingerprint, id.fingerprint);
|
|
assert.throws(() => identityFromP12(p12, 'wrong'), /Wrong password/);
|
|
});
|
|
|
|
test('store migrates the legacy identity and still asks for setup', async () => {
|
|
const dir = tmp();
|
|
const legacy = generateIdentity('legacy user');
|
|
writeFileSync(path.join(dir, 'identity.json'), JSON.stringify(legacy));
|
|
const store = new IdentityStore(dir);
|
|
const { identities, setupDone } = await store.list();
|
|
assert.equal(setupDone, false);
|
|
assert.equal(identities.length, 1);
|
|
assert.equal(identities[0].fingerprint, legacy.fingerprint);
|
|
assert.equal(identities[0].name, 'legacy user');
|
|
assert.ok(identities[0].isDefault);
|
|
assert.equal(statSync(path.join(dir, 'identities.json')).mode & 0o077, 0, 'identities file is owner-only');
|
|
await store.finishSetup();
|
|
assert.equal((await new IdentityStore(dir).list()).setupDone, true);
|
|
});
|
|
|
|
test('store create, import, duplicates, default and remove', async () => {
|
|
const store = new IdentityStore(tmp());
|
|
const a = await store.create('alice', '');
|
|
const other = generateIdentity('bob');
|
|
const b = await store.importP12(identityToP12(other, 'pw', 'bob'), 'pw', '');
|
|
assert.equal(b.name, 'bob');
|
|
await assert.rejects(store.importP12(identityToP12(other, 'x', 'bob'), 'x', ''), /already stored/);
|
|
assert.equal((await store.get()).id, a.id, 'first identity is the default');
|
|
assert.equal((await store.get(b.id)).fingerprint, other.fingerprint);
|
|
await store.setDefault(b.id);
|
|
await store.remove(b.id);
|
|
const { identities } = await store.list();
|
|
assert.deepEqual(identities.map(i => [i.name, i.isDefault]), [['alice', true]]);
|
|
});
|
|
|
|
test('imports the desktop Mumble client certificate', async () => {
|
|
const cfg = tmp();
|
|
const mumble = generateIdentity('Mumble User');
|
|
mkdirSync(path.join(cfg, 'Mumble', 'Mumble'), { recursive: true });
|
|
const b64 = Buffer.from(identityToP12(mumble, '', 'Mumble User')).toString('base64');
|
|
writeFileSync(path.join(cfg, 'Mumble', 'Mumble', 'mumble_settings.json'), JSON.stringify({ certificate: b64 }));
|
|
const prev = { xdg: process.env.XDG_CONFIG_HOME, appdata: process.env.APPDATA, home: process.env.HOME };
|
|
process.env.XDG_CONFIG_HOME = cfg;
|
|
process.env.APPDATA = path.join(cfg, 'none');
|
|
process.env.HOME = path.join(cfg, 'none');
|
|
try {
|
|
const store = new IdentityStore(tmp());
|
|
assert.deepEqual(await store.findMumbleCertificate().then(f => f?.name), 'Mumble User');
|
|
const imported = await store.importFromMumble('');
|
|
assert.equal(imported.fingerprint, mumble.fingerprint);
|
|
} finally {
|
|
for (const [k, v] of Object.entries({ XDG_CONFIG_HOME: prev.xdg, APPDATA: prev.appdata, HOME: prev.home })) {
|
|
if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
|
}
|
|
}
|
|
});
|