import { DEVICE_ID_MAX_BYTES, DEVICE_ID_PREFIX, DEVICE_ID_RANDOM_CHARS, DEVICE_ID_STORAGE_KEY, DeviceIdUnavailableError, isValidDeviceId, newDeviceId, } from '../src/lib/deviceId'; import { ANDROID_PHONE_LABEL, buildDeviceLabel } from '../src/lib/deviceLabel'; function fixedSource(value: string) { return jest.fn(async (_chars: number) => value); } describe('device id', () => { it('is a dev_-prefixed hex token of the expected size', async () => { const hex = 'a1b2c3d4e5f60718293a4b5c'; const id = await newDeviceId(fixedSource(hex)); expect(id).toBe(`${DEVICE_ID_PREFIX}${hex}`); expect(id).toHaveLength(DEVICE_ID_PREFIX.length + DEVICE_ID_RANDOM_CHARS); expect(id).toMatch(/^dev_[0-9a-f]{24}$/); }); it('keeps the `dev_` prefix and stores under a stable key', () => { expect(DEVICE_ID_PREFIX).toBe('dev_'); expect(DEVICE_ID_STORAGE_KEY).toBe('alarm.deviceId.v1'); }); it('refuses to build an id when the random source is unavailable', async () => { await expect(newDeviceId(async () => null)).rejects.toBeInstanceOf(DeviceIdUnavailableError); // The real bridge has no native module in a JS-only test run, so the default // source must fail loudly instead of falling back to Math.random(). await expect(newDeviceId()).rejects.toBeInstanceOf(DeviceIdUnavailableError); }); it('rejects sources that are not the requested size or not hex', async () => { await expect(newDeviceId(fixedSource('abc'))).rejects.toBeInstanceOf(DeviceIdUnavailableError); await expect( newDeviceId(fixedSource('zzzzzzzzzzzzzzzzzzzzzzzz')), ).rejects.toBeInstanceOf(DeviceIdUnavailableError); }); it('satisfies the service rule: 1..128 bytes, no control characters', async () => { const id = await newDeviceId(fixedSource('0123456789abcdef01234567')); expect(id.length).toBeGreaterThanOrEqual(1); expect(id.length).toBeLessThanOrEqual(DEVICE_ID_MAX_BYTES); // Mirrors Alarm/src/actions/errors.go:190-205. for (let i = 0; i < id.length; i++) { const code = id.charCodeAt(i); expect(code).toBeGreaterThanOrEqual(0x20); expect(code).not.toBe(0x7f); } expect(isValidDeviceId(id)).toBe(true); }); it('validates exactly like the service', () => { expect(isValidDeviceId('dev_abc123')).toBe(true); expect(isValidDeviceId(`dev_${'a'.repeat(DEVICE_ID_MAX_BYTES - 4)}`)).toBe(true); expect(isValidDeviceId(`dev_${'a'.repeat(DEVICE_ID_MAX_BYTES - 3)}`)).toBe(false); expect(isValidDeviceId('')).toBe(false); expect(isValidDeviceId(' ')).toBe(false); expect(isValidDeviceId('bad\u0000id')).toBe(false); expect(isValidDeviceId('bad\u001fid')).toBe(false); expect(isValidDeviceId('bad\u007fid')).toBe(false); expect(isValidDeviceId(42 as unknown)).toBe(false); expect(isValidDeviceId(null as unknown)).toBe(false); }); it('contains no hardware, model or install-time basis', async () => { // A generated id is exactly the prefix plus random hex: nothing from the // environment can leak in, because nothing from the environment is read. const id = await newDeviceId(fixedSource('deadbeefdeadbeefdeadbeef')); expect(id).toBe('dev_deadbeefdeadbeefdeadbeef'); expect(id).not.toMatch(/android|sdk|serial|emulator|install/i); }); }); describe('device label', () => { it('names the device role, not the identity', async () => { const id = await newDeviceId(fixedSource('0123456789abcdef01234567')); const label = buildDeviceLabel(); expect(label).toBe(ANDROID_PHONE_LABEL); expect(label).not.toContain(DEVICE_ID_PREFIX); for (let i = 0; i <= id.length - 4; i++) { expect(label).not.toContain(id.slice(i, i + 4)); } }); it('can carry a per-alert ordinal for disambiguation', () => { expect(buildDeviceLabel(2)).toBe('Android phone 2'); expect(buildDeviceLabel(0)).toBe('Android phone'); expect(buildDeviceLabel(Number.NaN)).toBe('Android phone'); }); it('never derives the label from a hex token', () => { const label = buildDeviceLabel(); expect(label).toBe('Android phone'); expect(label).not.toMatch(/[0-9a-f]{4}/); }); });