• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

homebridge / HAP-NodeJS / 29209933200

12 Jul 2026 09:35PM UTC coverage: 66.188% (+0.03%) from 66.161%
29209933200

Pull #1122

github

web-flow
Merge b07fa8525 into 570eb69bc
Pull Request #1122: Fix: test harness rewritten around a connection-owning HAP client, removing the axios dependency.

1926 of 3395 branches covered (56.73%)

Branch coverage included in aggregate %.

127 of 142 new or added lines in 4 files covered. (89.44%)

13 existing lines in 1 file now uncovered.

6640 of 9547 relevant lines covered (69.55%)

223.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.2
/src/test-utils/HAPHTTPClient.ts
1
import assert from "assert";
4✔
2
import { once } from "events";
4✔
3
import { HeaderObject, HTTPParser } from "http-parser-js";
4✔
4
import { createConnection, Socket } from "net";
4✔
5
import { HAPMimeTypes, PairingStates, PairMethods, TLVValues } from "../internal-types";
6
import { HAPHTTPCode, HAPPairingHTTPCode } from "../lib/HAPServer";
7
import { PairingInformation, PermissionTypes } from "../lib/model/AccessoryInfo";
8
import { HAPEncryption, HAPUsername } from "../lib/util/eventedhttp";
9
import * as hapCrypto from "../lib/util/hapCrypto";
4✔
10
import { awaitEventOnce } from "../lib/util/promise-utils";
4✔
11
import * as tlv from "../lib/util/tlv";
4✔
12
import {
13
  AccessoriesResponse,
14
  CharacteristicId,
15
  CharacteristicsReadResponse,
16
  CharacteristicsWriteRequest,
17
  CharacteristicsWriteResponse,
18
  PrepareWriteRequest,
19
  ResourceRequest,
20
} from "../types";
21
import { HAPHTTPError } from "./HAPHTTPError";
4✔
22
import { TLVError } from "./tlvError";
4✔
23

24
export interface HTTPResponse<T = Buffer> {
25
  shouldKeepAlive: boolean;
26
  upgrade: boolean;
27
  statusCode: number;
28
  statusMessage: string;
29
  versionMajor: number;
30
  versionMinor: number;
31
  headers: Record<string, string>;
32
  body: T;
33
  trailers: string[];
34
}
35

36
export interface HTTPRequestOptions {
37
  /**
38
   * Content-Type header applied when a request body is present. Defaults to "application/json".
39
   */
40
  contentType?: string;
41
  /**
42
   * Additional request headers appended verbatim to the fixed header block (e.g. an authorization header).
43
   */
44
  headers?: Record<string, string>;
45
}
46

47
/**
48
 * A HAP HTTP client backed by a raw TCP connection it owns for the connection's entire lifetime - the client-side mirror
49
 * of the server's HAPConnection. Owning the socket end-to-end is what allows a single connection to carry the pairing
50
 * handshakes, the encrypted session traffic whose keys are bound to that exact connection, and unsolicited EVENT messages
51
 * the server pushes. A pooled http.Agent socket cannot serve this role: the agent's free-socket read guard destroys a
52
 * pooled connection the moment the server sends unsolicited data.
53
 */
54
export class HAPHTTPClient {
4✔
55
  /**
56
   * Upper bound in milliseconds for awaiting received bytes before failing. Generous relative to the real loopback
57
   * round-trip so it never trips on a healthy connection, while still turning a genuinely absent response into a loud
58
   * timeout instead of a silent empty buffer.
59
   */
60
  private static readonly RECEIVE_TIMEOUT = 2000;
4✔
61

62
  private readonly address: string;
63
  private readonly port: number;
64

65
  private currentSocket?: Socket;
66
  private encryption?: HAPEncryption;
67
  private socketClosed = false;
132✔
68
  private everConnected = false;
132✔
69

70
  private currentDataListener?: (data: Buffer) => void;
71
  private dataQueue: Buffer[] = [];
132✔
72
  // Already-decrypted bytes trailing a parsed HTTP response that shared a TCP segment with subsequent data (e.g. a
73
  // coalesced EVENT message). Consumed by popReceiveBuffer before any further (still encrypted) segments.
74
  private leftoverPlaintext?: Buffer;
75

76
  constructor(address: string, port: number) {
77
    this.address = address;
132✔
78
    this.port = port;
132✔
79
  }
80

81
  /**
82
   * Opens the TCP connection this client owns for its entire lifetime and installs the receive machinery. Mirrors the
83
   * server-side HAPConnection setup: Nagle is disabled so small request/response writes are not artificially delayed.
84
   */
85
  async connect(): Promise<void> {
86
    // One client instance models one connection for its entire lifetime - the client-side mirror of the server's
87
    // per-connection HAPConnection - so a client is never reconnected after use. A test needing a fresh connection
88
    // constructs a fresh client, which also keeps all receive state trivially clean.
89
    expect(this.everConnected).toBe(false);
132✔
90
    this.everConnected = true;
132✔
91

92
    const socket = createConnection(this.port, this.address);
132✔
93
    socket.setNoDelay(true);
132✔
94

95
    // A permanent error listener must exist for the socket's lifetime: without one, a late ECONNRESET (e.g. the server
96
    // tearing down first in afterEach) would crash the process. Failures surface loudly through receive timeouts instead.
97
    socket.on("error", () => {});
132✔
98
    socket.on("close", () => {
132✔
99
      this.socketClosed = true;
128✔
100
    });
101

102
    this.currentDataListener = data => {
132✔
103
      this.dataQueue.push(data);
284✔
104
    };
105
    socket.on("data", this.currentDataListener);
132✔
106
    this.currentSocket = socket;
132✔
107

108
    // once() rejects on a socket "error" while waiting, covering the failed-connect path.
109
    await once(socket, "connect");
132✔
110
  }
111

112
  /**
113
   * Destroys the owned connection. Safe to call multiple times and on a never-connected client, so tests can place it in
114
   * finally blocks and afterEach hooks unconditionally.
115
   */
116
  destroy(): void {
117
    if (this.currentSocket) {
136✔
118
      if (this.currentDataListener) {
132!
119
        this.currentSocket.removeListener("data", this.currentDataListener);
132✔
120
        this.currentDataListener = undefined;
132✔
121
      }
122
      this.currentSocket.destroy();
132✔
123
      this.currentSocket = undefined;
132✔
124
    }
125
  }
126

127
  /**
128
   * True once the underlying socket has fully closed - regardless of which side initiated it. Lets tests assert
129
   * server-initiated teardown directly on the connection instead of inferring it from client-library internals.
130
   */
131
  get isClosed(): boolean {
132
    return this.socketClosed;
2✔
133
  }
134

135
  /**
136
   * Resolves once the underlying socket has fully closed, rejecting after {@link timeoutMs} if it never does. The direct
137
   * observation primitive for tests asserting that the server tears a connection down.
138
   */
139
  async waitForClose(timeoutMs = HAPHTTPClient.RECEIVE_TIMEOUT): Promise<void> {
4✔
140
    if (this.socketClosed) {
4!
NEW
141
      return;
×
142
    }
143
    expect(this.currentSocket).toBeDefined();
4✔
144
    await awaitEventOnce(this.currentSocket!, "close", timeoutMs);
4✔
145
  }
146

147
  get receiveBufferCount(): number {
148
    return this.dataQueue.length + (this.leftoverPlaintext ? 1 : 0);
310!
149
  }
150

151
  enableEncryption(encryption: HAPEncryption): void {
152
    this.encryption = encryption;
50✔
153
  }
154

155
  disableEncryption(): void {
156
    this.encryption = undefined;
×
157
  }
158

159
  popReceiveBuffer(): Buffer {
160
    expect(this.currentSocket).toBeDefined();
284✔
161
    if (this.leftoverPlaintext) {
284!
NEW
162
      const buffer = this.leftoverPlaintext;
×
NEW
UNCOV
163
      this.leftoverPlaintext = undefined;
×
NEW
UNCOV
164
      return buffer;
×
165
    }
166
    expect(this.dataQueue.length > 0).toBeTruthy();
284✔
167
    const buffer = this.dataQueue.splice(0, 1)[0];
284✔
168
    if (this.encryption) {
284✔
169
      return hapCrypto.layerDecrypt(buffer, this.encryption);
54✔
170
    }
171
    return buffer;
230✔
172
  }
173

174
  /**
175
   * Waits for the next TCP segment to arrive, rejecting once {@link deadline} (an absolute epoch-milliseconds timestamp)
176
   * passes or the connection closes. A closed socket can never deliver the awaited bytes, so failing fast on close beats
177
   * stalling out the timeout and obscuring the real cause.
178
   */
179
  private awaitIncomingData(deadline: number): Promise<void> {
180
    expect(this.currentSocket).toBeDefined();
284✔
181

182
    const socket = this.currentSocket!;
284✔
183

184
    return new Promise((resolve, reject) => {
284✔
185
      let settled = false;
284✔
186

187
      const cleanups: (() => void)[] = [];
284✔
188

189
      // Whichever of the three outcomes settles first tears down the other two registrations, so no timer or listener
190
      // outlives the wait.
191
      const settle = (error?: Error) => {
284✔
192
        if (settled) {
284!
NEW
193
          return;
×
194
        }
195
        settled = true;
284✔
196
        for (const cleanup of cleanups) {
284✔
197
          cleanup();
852✔
198
        }
199
        if (error) {
284!
NEW
200
          reject(error);
×
201
        } else {
202
          resolve();
284✔
203
        }
204
      };
205

206
      const timeoutId = setTimeout(() => settle(new Error("Timed out awaiting incoming data on the connection.")), Math.max(0, deadline - Date.now()));
284✔
207
      const onData = () => settle();
284✔
208
      const onClose = () => settle(new Error("Connection closed while awaiting incoming data."));
284✔
209

210
      cleanups.push(() => clearTimeout(timeoutId));
284✔
211
      cleanups.push(() => socket.removeListener("data", onData));
284✔
212
      cleanups.push(() => socket.removeListener("close", onClose));
284✔
213

214
      socket.on("data", onData);
284✔
215
      socket.on("close", onClose);
284✔
216
    });
217
  }
218

219
  /**
220
   * Resolves once at least {@link count} buffers have arrived - immediately if they already have - and rejects once
221
   * {@link timeoutMs} has elapsed overall or the connection closes while waiting. Callers await the genuine arrival of
222
   * bytes instead of sleeping for a fixed interval and hoping the loopback encrypt/decrypt round-trip has completed by
223
   * then; that is what keeps the suite robust across Node major versions whose event-loop scheduling differs, and it lets
224
   * a genuinely missing response fail loudly instead of silently reading an empty buffer.
225
   */
226
  async waitForData(count = 1, timeoutMs = HAPHTTPClient.RECEIVE_TIMEOUT): Promise<void> {
6!
227
    const deadline = Date.now() + timeoutMs;
8✔
228

229
    while (this.receiveBufferCount < count) {
8✔
230
      await this.awaitIncomingData(deadline);
8✔
231
    }
232
  }
233

234
  /**
235
   * Drains and returns every buffered message as one string once the received data contains {@link marker}, awaiting
236
   * further TCP segments until it does (rejecting once {@link timeoutMs} has elapsed overall or the connection closes).
237
   * HomeKit may coalesce an HTTP response and a trailing EVENT message into a single TCP segment or split them across
238
   * two, so matching on content rather than on a fixed segment count keeps event assertions independent of that
239
   * nondeterminism.
240
   */
241
  async readUntil(marker: string, timeoutMs = HAPHTTPClient.RECEIVE_TIMEOUT): Promise<string> {
2✔
242
    const deadline = Date.now() + timeoutMs;
4✔
243

244
    let received = "";
4✔
245
    while (this.receiveBufferCount > 0) {
4✔
NEW
246
      received += this.popReceiveBuffer().toString();
×
247
    }
248
    while (!received.includes(marker)) {
4✔
249
      await this.awaitIncomingData(deadline);
4✔
250
      while (this.receiveBufferCount > 0) {
4✔
251
        received += this.popReceiveBuffer().toString();
4✔
252
      }
253
    }
254
    return received;
4✔
255
  }
256

257
  formatHTTPRequest(
258
    method: "GET" | "POST" | "PUT" | "DELETE",
259
    route: string,
260
    data?: Buffer,
261
    options: HTTPRequestOptions = {},
70✔
262
  ): Buffer {
263
    const contentType = options.contentType ?? "application/json";
278✔
264

265
    let extraHeaders = "";
278✔
266
    for (const [name, value] of Object.entries(options.headers ?? {})) {
278✔
267
      extraHeaders += name + ": " + value + "\r\n";
8✔
268
    }
269

270
    // A body-less non-GET request is legitimate (e.g. POST /identify) and carries an explicit zero Content-Length so the
271
    // server's parser can frame the message without ambiguity.
272
    const bodyHeaders = data
278✔
273
      ? "Content-Type: " + contentType + "\r\n" + "Content-Length: " + data.length + "\r\n"
274
      : (method === "GET" ? "" : "Content-Length: 0\r\n");
68✔
275

276
    const buffer = Buffer.from(`${method} ${route} HTTP/1.1\r\n` +
278✔
277
      "Accept: application/json, text/plain, */*\r\n" +
278
      "User-Agent: test-util\r\n" +
279
      "Host: " + this.address + ":" + this.port + "\r\n" +
280
      "Connection: keep-alive\r\n" +
281
      extraHeaders +
282
      bodyHeaders +
283
      "\r\n");
284

285
    if (data) {
278✔
286
      return Buffer.concat([buffer, data]);
210✔
287
    }
288

289
    return buffer;
68✔
290
  }
291

292
  async writeHTTPRequest(method: "GET" | "POST" | "PUT" | "DELETE", route: string, data?: Buffer, options?: HTTPRequestOptions): Promise<HTTPResponse> {
293
    const httpRequest = this.formatHTTPRequest(method, route, data, options);
268✔
294
    this.write(httpRequest);
268✔
295
    return this.readHTTPResponse();
268✔
296
  }
297

298
  write(data: Buffer): void {
299
    if (this.encryption) {
278✔
300
      data = hapCrypto.layerEncrypt(data, this.encryption);
54✔
301
    }
302
    expect(this.currentSocket).toBeDefined();
278✔
303
    this.currentSocket!.write(data);
278✔
304
  }
305

306
  /**
307
   * Reads exactly one complete HTTP response off the connection, feeding the parser segment by segment until the message
308
   * completes (rejecting once {@link timeoutMs} has elapsed overall or the connection closes). Parsing incrementally
309
   * removes any assumption about how the runtime segments the response across TCP packets - segmentation is exactly the
310
   * kind of behavior that shifts between Node major versions. Bytes trailing the parsed message (e.g. a coalesced EVENT
311
   * notification) are retained for the next read rather than lost.
312
   */
313
  async readHTTPResponse(timeoutMs = HAPHTTPClient.RECEIVE_TIMEOUT): Promise<HTTPResponse> {
272✔
314
    const deadline = Date.now() + timeoutMs;
272✔
315
    const parser = new HTTPParser(HTTPParser.RESPONSE);
272✔
316

317
    let complete = false;
272✔
318
    // The chunk-relative position at which the message completed. The parser does not stop at message end - it
319
    // reinitializes and keeps consuming trailing bytes as the start of a next message within the same execute() call -
320
    // so the boundary must be captured the moment kOnMessageComplete fires, and the callbacks below must go quiet once
321
    // the message of interest is complete so trailing bytes cannot corrupt the parsed result.
322
    let completedAt = 0;
272✔
323
    let shouldKeepAlive = false;
272✔
324
    let upgrade = false;
272✔
325
    let statusCode = 0;
272✔
326
    let statusMessage = "";
272✔
327
    let versionMajor = 0;
272✔
328
    let versionMinor = 0;
272✔
329
    let headers: HeaderObject = [];
272✔
330
    let trailers: string[] = [];
272✔
331
    const bodyChunks: Buffer[] = [];
272✔
332

333
    parser[HTTPParser.kOnHeadersComplete] = info => {
272✔
334
      if (complete) {
272!
NEW
335
        return;
×
336
      }
337
      shouldKeepAlive = info.shouldKeepAlive;
272✔
338
      upgrade = info.upgrade;
272✔
339
      statusCode = info.statusCode;
272✔
340
      statusMessage = info.statusMessage;
272✔
341
      versionMajor = info.versionMajor;
272✔
342
      versionMinor = info.versionMinor;
272✔
343
      headers = info.headers;
272✔
344
    };
345

346
    parser[HTTPParser.kOnBody] = (chunk, offset, length) => {
272✔
347
      if (complete) {
266!
NEW
348
        return;
×
349
      }
350
      bodyChunks.push(chunk.subarray(offset, offset + length));
266✔
351
    };
352

353
    // that's the event for trailers!
354
    parser[HTTPParser.kOnHeaders] = t => {
272✔
NEW
355
      if (complete) {
×
NEW
356
        return;
×
357
      }
NEW
358
      trailers = t;
×
359
    };
360

361
    parser[HTTPParser.kOnMessageComplete] = () => {
272✔
362
      complete = true;
272✔
363
      // The parser's chunk-relative read position at completion time is the exact message boundary. The property is
364
      // runtime state the type declarations do not surface, hence the narrow structural cast.
365
      completedAt = (parser as unknown as { offset: number }).offset;
272✔
366
    };
367

368
    while (!complete) {
272✔
369
      if (this.receiveBufferCount === 0) {
272!
370
        await this.awaitIncomingData(deadline);
272✔
371
      }
372

373
      const chunk = this.popReceiveBuffer();
272✔
374
      const result = parser.execute(chunk);
272✔
375

376
      if (complete) {
272!
377
        // Bytes beyond the completed message belong to the next message (e.g. a coalesced EVENT notification): retain
378
        // them for the next read. A parse error the parser hit on those trailing bytes is not this response's concern.
379
        if (completedAt < chunk.length) {
272!
NEW
380
          this.leftoverPlaintext = chunk.subarray(completedAt);
×
381
        }
NEW
382
      } else if (typeof result !== "number") {
×
NEW
383
        throw new Error("Failed to parse HTTP response chunk: " + result.message);
×
384
      }
385
    }
386

387
    const body = Buffer.concat(bodyChunks);
272✔
388

389
    return {
272✔
390
      shouldKeepAlive,
391
      upgrade,
392
      statusCode,
393
      statusMessage,
394
      versionMajor,
395
      versionMinor,
396
      headers: this.headersArrayToObject(headers),
397
      body,
398
      trailers,
399
    };
400
  }
401

402
  async sendAddPairingRequest(identifier: HAPUsername, publicKey: Buffer, permission: PermissionTypes): Promise<void> {
403
    const requestTLV = tlv.encode(
2✔
404
      TLVValues.METHOD, PairMethods.ADD_PAIRING,
405
      TLVValues.STATE, PairingStates.M1,
406
      TLVValues.IDENTIFIER, identifier,
407
      TLVValues.PUBLIC_KEY, publicKey,
408
      TLVValues.PERMISSIONS, permission,
409
    );
410

411
    await this.sendPairingsRequest(requestTLV);
2✔
412
  }
413

414
  async sendRemovePairingRequest(identifier: HAPUsername): Promise<void> {
415
    const requestTLV = tlv.encode(
2✔
416
      TLVValues.METHOD, PairMethods.REMOVE_PAIRING,
417
      TLVValues.STATE, PairingStates.M1,
418
      TLVValues.IDENTIFIER, identifier,
419
    );
420

421
    await this.sendPairingsRequest(requestTLV);
2✔
422
  }
423

424
  async sendListPairingsRequest(): Promise<PairingInformation[]> {
425
    const requestTLV = tlv.encode(
2✔
426
      TLVValues.METHOD, PairMethods.LIST_PAIRINGS,
427
      TLVValues.STATE, PairingStates.M1,
428
    );
429

430
    const responseBody = await this.sendPairingsRequest(requestTLV);
2✔
431
    const tlvDataList = tlv.decodeList(responseBody.subarray(3), TLVValues.IDENTIFIER);
2✔
432

433
    const result: PairingInformation[] = [];
2✔
434

435
    for (const element of tlvDataList) {
2✔
436
      result.push({
4✔
437
        username: element[TLVValues.IDENTIFIER].toString(),
438
        publicKey: element[TLVValues.PUBLIC_KEY],
439
        permission: element[TLVValues.PERMISSIONS].readUInt8(0),
440
      });
441
    }
442

443
    return result;
2✔
444
  }
445

446
  private async sendPairingsRequest(requestTLV: Buffer): Promise<Buffer> {
447
    const httpResponse = await this.writeHTTPRequest("POST", "/pairings", requestTLV, { contentType: HAPMimeTypes.PAIRING_TLV8 });
6✔
448

449
    // `/pairings` errors are transported via the tlv8 record
450
    expect(httpResponse.statusCode).toEqual(HAPPairingHTTPCode.OK);
6✔
451
    expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.PAIRING_TLV8);
6✔
452

453
    const tlvData = tlv.decode(httpResponse.body);
6✔
454
    expect(tlvData[TLVValues.STATE].readUInt8(0)).toEqual(PairingStates.M2);
6✔
455

456
    if (tlvData[TLVValues.ERROR_CODE]) {
6!
UNCOV
457
      throw new TLVError(tlvData[TLVValues.ERROR_CODE].readUInt8(0));
×
458
    }
459

460
    // we return the raw buffer because LIST_PAIRINGS has some custom decoding strategies!
461
    return httpResponse.body;
6✔
462
  }
463

464
  public async sendAccessoriesRequest(): Promise<AccessoriesResponse> {
465
    const httpResponse = await this.writeHTTPRequest("GET", "/accessories");
4✔
466
    expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON);
4✔
467
    const jsonBody = JSON.parse(httpResponse.body.toString());
4✔
468

469
    if (httpResponse.statusCode !== HAPPairingHTTPCode.OK) {
4!
UNCOV
470
      throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status);
×
471
    }
472

473
    return jsonBody;
4✔
474
  }
475

476
  public async sendCharacteristicRead(
477
    ids: CharacteristicId[],
478
    includeMeta?: boolean,
479
    includePerms?: boolean,
480
    includeType?: boolean,
481
    includeEvent?: boolean,
482
  ): Promise<HTTPResponse<CharacteristicsReadResponse>> {
483
    assert(ids.length > 0);
4✔
484
    let query = "?id=" + ids.map(id => id.aid + "." + id.iid).join(",");
8✔
485

486
    if (includeMeta) {
4!
UNCOV
487
      query += "&meta=" + (includeMeta ? "true" : "false");
×
488
    }
489
    if (includePerms) {
4!
490
      query += "&perms=" + (includePerms ? "1" : "0");
×
491
    }
492
    if (includeType) {
4✔
493
      query += "&type=" + (includeType ? "true" : "false");
2!
494
    }
495
    if (includeEvent) {
4✔
496
      query += "&ev=" + (includeEvent ? "1" : "0");
2!
497
    }
498

499
    const httpResponse = await this.writeHTTPRequest("GET", "/characteristics" + query);
4✔
500
    expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON);
4✔
501

502
    const body = JSON.parse(httpResponse.body.toString());
4✔
503
    if (!httpResponse.statusCode.toString().startsWith("2")) {
4!
UNCOV
504
      throw new HAPHTTPError(httpResponse.statusCode, body.status);
×
505
    }
506

507
    return {
4✔
508
      ...httpResponse,
509
      body: body,
510
    };
511
  }
512

513
  public async sendCharacteristicWrite(writeRequest: CharacteristicsWriteRequest): Promise<HTTPResponse<CharacteristicsWriteResponse | undefined>> {
514
    const httpResponse = await this.writeHTTPRequest(
6✔
515
      "PUT", "/characteristics", Buffer.from(JSON.stringify(writeRequest)), { contentType: HAPMimeTypes.HAP_JSON },
516
    );
517
    if (httpResponse.statusCode !== HAPHTTPCode.NO_CONTENT) {
6✔
518
      expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON);
4✔
519
    }
520

521
    if (!httpResponse.statusCode.toString().startsWith("2")) {
6!
UNCOV
522
      const jsonBody = JSON.parse(httpResponse.body.toString());
×
UNCOV
523
      throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status);
×
524
    }
525

526
    return {
6✔
527
      ...httpResponse,
528
      body: httpResponse.body.length > 0 ? JSON.parse(httpResponse.body.toString()) : undefined,
6✔
529
    };
530
  }
531

532
  public async sendPrepareWrite(prepareWrite: PrepareWriteRequest): Promise<void> {
533
    const httpResponse = await this.writeHTTPRequest(
4✔
534
      "PUT", "/prepare", Buffer.from(JSON.stringify(prepareWrite)), { contentType: HAPMimeTypes.HAP_JSON },
535
    );
536
    expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON);
4✔
537

538
    if (httpResponse.statusCode !== HAPHTTPCode.OK) {
4!
UNCOV
539
      const jsonBody = JSON.parse(httpResponse.body.toString());
×
UNCOV
540
      throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status);
×
541
    }
542
  }
543

544
  public async sendResourceRequest(resourceRequest: ResourceRequest): Promise<Buffer> {
545
    const httpResponse = await this.writeHTTPRequest(
2✔
546
      "POST", "/resource", Buffer.from(JSON.stringify(resourceRequest)), { contentType: HAPMimeTypes.HAP_JSON },
547
    );
548

549
    if (httpResponse.statusCode !== HAPHTTPCode.OK) {
2!
UNCOV
550
      expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.HAP_JSON);
×
UNCOV
551
      const jsonBody = JSON.parse(httpResponse.body.toString());
×
UNCOV
552
      throw new HAPHTTPError(httpResponse.statusCode, jsonBody.status);
×
553
    }
554

555
    expect(httpResponse.headers["Content-Type"]).toEqual(HAPMimeTypes.IMAGE_JPEG);
2✔
556
    return httpResponse.body;
2✔
557
  }
558

559
  private headersArrayToObject(headers: HeaderObject): Record<string, string> {
560
    expect(headers.length % 2).toBe(0);
272✔
561

562
    const result: Record<string, string> = {};
272✔
563

564
    for (let i = 0; i < headers.length; i += 2) {
272✔
565
      result[headers[i]] = headers[i+1];
1,076✔
566
    }
567

568
    return result;
272✔
569
  }
570
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc