From daeac3d6313c19c2d5e3388cce2440c27ed46f34 Mon Sep 17 00:00:00 2001 From: Marcel Hild Date: Mon, 9 Mar 2026 09:19:43 +0100 Subject: [PATCH] test: add regression test for password-mode device signing Covers the case where authMode is 'password' and deviceIdentity is present. Asserts that buildDeviceAuthPayload is called with token:null (not the password value) and that the connect request uses auth.password, not auth.token. Addresses Copilot review comment on PR #22. --- src/lib/__tests__/gateway.test.ts | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/lib/__tests__/gateway.test.ts b/src/lib/__tests__/gateway.test.ts index 54138db..67a8692 100644 --- a/src/lib/__tests__/gateway.test.ts +++ b/src/lib/__tests__/gateway.test.ts @@ -1,5 +1,12 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { GatewayClient } from '../gateway'; +import type { DeviceIdentity } from '../deviceIdentity'; +import * as deviceIdentityModule from '../deviceIdentity'; + +vi.mock('../deviceIdentity', () => ({ + buildDeviceAuthPayload: vi.fn(), + signPayload: vi.fn(), +})); /* ------------------------------------------------------------------ */ /* Minimal WebSocket mock */ @@ -50,6 +57,7 @@ beforeEach(() => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).WebSocket = MockWebSocket; vi.useFakeTimers(); + vi.clearAllMocks(); }); afterEach(() => { @@ -297,6 +305,45 @@ describe('GatewayClient', () => { gw.disconnect(); }); + it('password mode with deviceIdentity: signs with token:null and sends auth.password', async () => { + const buildPayload = vi.mocked(deviceIdentityModule.buildDeviceAuthPayload); + const sign = vi.mocked(deviceIdentityModule.signPayload); + buildPayload.mockReturnValue('mock-device-payload'); + sign.mockResolvedValue('mock-sig'); + + // In password mode authToken holds the password string, not a JWT/token. + // buildDeviceAuthPayload must receive token:null so the gateway signature + // verification matches (gateway sees no token segment in the connect request). + const gw = new GatewayClient('ws://test:1234', 'my-secret-password', 'password'); + const mockIdentity: DeviceIdentity = { + id: 'device-id-abc', + publicKeyRaw: 'pubkey-raw-abc', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + keyPair: { privateKey: {} as CryptoKey, publicKey: {} as CryptoKey }, + }; + gw.setDeviceIdentity(mockIdentity); + + gw.connect(); + await vi.advanceTimersByTimeAsync(1); + + const ws = MockWebSocket.instances[0]!; + ws._receive({ type: 'event', event: 'connect.challenge', payload: { nonce: 'nonce-xyz' } }); + await vi.advanceTimersByTimeAsync(0); + + // buildDeviceAuthPayload must be called with token: null (not the password) + expect(buildPayload).toHaveBeenCalledWith( + expect.objectContaining({ token: null }), + ); + + // The connect request must use auth.password, not auth.token + const req = JSON.parse(ws.sent[0]!); + expect(req.method).toBe('connect'); + expect(req.params.auth.password).toBe('my-secret-password'); + expect(req.params.auth.token).toBeUndefined(); + + gw.disconnect(); + }); + it('emits pairing status on NOT_PAIRED error', async () => { const gw = new GatewayClient('ws://test:1234', 'tok'); const statuses: string[] = [];