/** * Tests for electron/mobile-lan-cert.ts — the dependency-free self-signed * certificate of the "Telefon bağlantısı" LAN proxy and its vault storage. */ import crypto from 'node:crypto' import { describe, expect, it } from 'vitest' import { createSelfSignedCertificate, fingerprintOfPem, isUsableIdentity, loadOrCreateLanIdentity } from './mobile-lan-cert' function memoryVault(initial: Record = {}) { const state = { value: structuredClone(initial), saves: 0 } return { state, load: async () => structuredClone(state.value), save: async (value: Record) => { state.value = structuredClone(value) state.saves++ } } } describe('createSelfSignedCertificate', () => { it('produces a parseable, self-verifying ECDSA P-256 X.509 certificate', () => { const identity = createSelfSignedCertificate() const x509 = new crypto.X509Certificate(identity.cert) expect(x509.verify(x509.publicKey)).toBe(true) expect(x509.publicKey.asymmetricKeyType).toBe('ec') expect(x509.publicKey.asymmetricKeyDetails?.namedCurve).toBe('prime256v1') expect(x509.checkPrivateKey(crypto.createPrivateKey(identity.key))).toBe(true) expect(new Date(x509.validTo).getTime()).toBeGreaterThan(Date.now() + 3000 * 24 * 3600e3) expect(new Date(x509.validFrom).getTime()).toBeLessThan(Date.now()) }) it('fingerprint is the base64url SHA-256 of the DER bytes (43 characters, no padding)', () => { const identity = createSelfSignedCertificate() const der = new crypto.X509Certificate(identity.cert).raw expect(identity.fingerprint).toMatch(/^[A-Za-z0-9_-]{43}$/) expect(identity.fingerprint).toBe(crypto.createHash('sha256').update(der).digest('base64url')) expect(fingerprintOfPem(identity.cert)).toBe(identity.fingerprint) expect(createSelfSignedCertificate().fingerprint).not.toBe(identity.fingerprint) }) it('refuses a validity that UTCTime cannot carry', () => { expect(() => createSelfSignedCertificate({ now: new Date('2045-01-01T00:00:00Z') })).toThrow( 'mobile-lan-cert:validity-out-of-range' ) }) }) describe('loadOrCreateLanIdentity', () => { it('creates once, stores the key only in the vault, keeps the other vault fields, then reuses it', async () => { const vault = memoryVault({ accountToken: 'old-field', phoneFingerprints: { a: 'b' } }) const first = await loadOrCreateLanIdentity(vault) expect(first.created).toBe(true) expect(vault.state.value.accountToken).toBe('old-field') expect(vault.state.value.phoneFingerprints).toEqual({ a: 'b' }) expect(vault.state.value.lanTls.key).toContain('PRIVATE KEY') const second = await loadOrCreateLanIdentity(vault) expect(second.created).toBe(false) expect(second.fingerprint).toBe(first.fingerprint) expect(vault.state.saves).toBe(1) }) it('replaces a corrupt, mismatched or nearly expired identity', async () => { const good = createSelfSignedCertificate() const other = createSelfSignedCertificate() expect(isUsableIdentity(good)).toBe(true) expect(isUsableIdentity({ ...good, key: other.key })).toBe(false) expect(isUsableIdentity({ ...good, fingerprint: other.fingerprint })).toBe(false) expect(isUsableIdentity({ ...good, cert: 'not a certificate' })).toBe(false) expect(isUsableIdentity(good, new Date(Date.now() + 3640 * 24 * 3600e3))).toBe(false) const vault = memoryVault({ lanTls: { ...good, key: other.key } }) const renewed = await loadOrCreateLanIdentity(vault) expect(renewed.created).toBe(true) expect(renewed.fingerprint).not.toBe(good.fingerprint) }) it('surfaces a vault failure instead of running without a stored key', async () => { const vault = { load: async () => { throw new Error('vault closed') }, save: async () => undefined } await expect(loadOrCreateLanIdentity(vault)).rejects.toThrow('vault closed') }) })