Skip to content

Commit 517e33b

Browse files
bwp91claude
andcommitted
test: cover error argument in pairing debug logs
Adds two HAPServer integration tests verifying the fix from f12ed23: when M5 / pair-verify M3 decryption fails, the debug() call must receive the caught error as its second %s argument. Tests intercept the global debug.log function and assert the captured args include the username AND the error object — without the fix only the username was passed and %s rendered as a literal trailing \"undefined\" / blank. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 72f52e0 commit 517e33b

2 files changed

Lines changed: 99 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ All notable changes to `@homebridge/hap-nodejs` will be documented in this file.
1717
- test: cover safe accessory lookups in slow/timeout warnings
1818
- test: cover aid.iid format validation
1919
- test: cover camera stream start TLV parsing guards
20+
- test: cover error argument in pairing debug logs
2021

2122
### Homebridge Dependencies
2223

src/lib/HAPServer.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import axios, { AxiosError, AxiosResponse } from "axios";
22
import crypto from "crypto";
3+
import createDebug from "debug";
34
import { Agent } from "http";
45
import tweetnacl from "tweetnacl";
56
import { PairingStates, PairMethods, TLVValues } from "../internal-types";
@@ -328,6 +329,103 @@ describe("HAPServer", () => {
328329
});
329330
});
330331

332+
describe("error argument in pairing debug logs (fix f12ed233)", () => {
333+
let originalEnable: string;
334+
let originalLog: (...args: unknown[]) => void;
335+
let captured: unknown[][];
336+
337+
beforeEach(() => {
338+
captured = [];
339+
// createDebug.disable() returns the namespaces it just disabled, so we
340+
// can faithfully restore them via createDebug.enable() in afterEach.
341+
originalEnable = createDebug.disable();
342+
// route all enabled debug output through our capture function
343+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
344+
originalLog = (createDebug as any).log;
345+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
346+
(createDebug as any).log = function(this: { namespace: string }, ...args: unknown[]) {
347+
captured.push([this.namespace, ...args]);
348+
};
349+
createDebug.enable("HAP-NodeJS:HAPServer");
350+
});
351+
352+
afterEach(() => {
353+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
354+
(createDebug as any).log = originalLog;
355+
createDebug.disable();
356+
if (originalEnable) {
357+
createDebug.enable(originalEnable);
358+
}
359+
});
360+
361+
test("M5 decrypt failure should pass the error to the debug logger", async () => {
362+
server = new HAPServer(accessoryInfoUnpaired);
363+
const [port] = await bindServer(server);
364+
365+
const pairSetup = new PairSetupClient(port, httpAgent);
366+
const responseM1 = await pairSetup.sendM1();
367+
const M2 = pairSetup.parseM2(responseM1.data);
368+
const M3 = await pairSetup.prepareM3(M2, accessoryInfoUnpaired.pincode);
369+
const responseM3 = await pairSetup.sendM3(M3);
370+
pairSetup.parseM4(responseM3.data, M3);
371+
372+
// craft an M5 with bogus ciphertext+tag (passes the >=16 length check
373+
// from 0719059b but fails MAC verification)
374+
await axios.post(
375+
`http://localhost:${port}/pair-setup`,
376+
tlv.encode(
377+
TLVValues.STATE, PairingStates.M5,
378+
TLVValues.ENCRYPTED_DATA, Buffer.alloc(48), // 32-byte payload + 16-byte tag, all zeros
379+
),
380+
{ httpAgent, responseType: "arraybuffer" },
381+
);
382+
383+
// captured rows: [namespace, formatString, ...formatArgs]
384+
const m5Lines = captured.filter(line => typeof line[1] === "string"
385+
&& (line[1] as string).includes("Error while decrypting and verifying M5 subTlv"));
386+
expect(m5Lines.length).toBeGreaterThan(0);
387+
388+
// the format string contains two %s — the first is the username, the
389+
// second is the error. Without the fix only the username was passed.
390+
const line = m5Lines[0];
391+
expect(line[2]).toBe(accessoryInfoUnpaired.username);
392+
// line[3] should be the error caught from decipher.final() — the
393+
// exact constructor varies by Node version / openssl binding, so
394+
// just assert it's a thrown error-like object with a message.
395+
expect(line[3]).toBeDefined();
396+
expect((line[3] as Error).message).toEqual(expect.any(String));
397+
});
398+
399+
test("pair-verify M3 decrypt failure should pass the error to the debug logger", async () => {
400+
server = new HAPServer(accessoryInfoPaired);
401+
const [port] = await bindServer(server);
402+
403+
const pairVerify = new PairVerifyClient(port, httpAgent);
404+
const responseM1 = await pairVerify.sendM1();
405+
pairVerify.parseM2(responseM1.data, serverInfoPaired);
406+
407+
await axios.post(
408+
`http://localhost:${port}/pair-verify`,
409+
tlv.encode(
410+
TLVValues.STATE, PairingStates.M3,
411+
TLVValues.ENCRYPTED_DATA, Buffer.alloc(48),
412+
),
413+
{ httpAgent, responseType: "arraybuffer" },
414+
);
415+
416+
const m3Lines = captured.filter(line => typeof line[1] === "string"
417+
&& (line[1] as string).includes("M3: Failed to decrypt and/or verify"));
418+
expect(m3Lines.length).toBeGreaterThan(0);
419+
const line = m3Lines[0];
420+
expect(line[2]).toBe(accessoryInfoPaired.username);
421+
// line[3] should be the error caught from decipher.final() — the
422+
// exact constructor varies by Node version / openssl binding, so
423+
// just assert it's a thrown error-like object with a message.
424+
expect(line[3]).toBeDefined();
425+
expect((line[3] as Error).message).toEqual(expect.any(String));
426+
});
427+
});
428+
331429
describe("M1 reset prevention (fix d4c81be0)", () => {
332430
test("/pair-setup should reject a second M1 on a connection with in-progress state", async () => {
333431
server = new HAPServer(accessoryInfoUnpaired);

0 commit comments

Comments
 (0)