From b6f3aba23ea10e1f2a0da5b2756b6a73be7bd3e1 Mon Sep 17 00:00:00 2001 From: ajnart Date: Sat, 22 Aug 2026 17:16:31 +0200 Subject: [PATCH] test(http,immich): prove User-Agent fix at the wire level Add two regression tests that fail on v1.76.0 and pass with the fix: - wire-level test: a Headers instance (the shape @immich/sdk hands over) must reach the remote server with all headers intact - real-SDK e2e: getServerStatistics through fetchWithTrustedCertificatesAsync against a live local server must deliver x-api-key --- .../test/infrastructure/http/request.spec.ts | 55 ++++++++++++++++++- .../integrations/test/immich-sdk-e2e.spec.ts | 37 +++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 packages/integrations/test/immich-sdk-e2e.spec.ts diff --git a/packages/core/src/test/infrastructure/http/request.spec.ts b/packages/core/src/test/infrastructure/http/request.spec.ts index 4d1f8527a..3d2b9c888 100644 --- a/packages/core/src/test/infrastructure/http/request.spec.ts +++ b/packages/core/src/test/infrastructure/http/request.spec.ts @@ -1,6 +1,18 @@ -import { describe, expect, test } from "vitest"; +// @vitest-environment node -import { mergeHeadersWithUserAgent } from "@homarr/core/infrastructure/http"; +import { createServer } from "node:http"; +import type { Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import type { RequestInit } from "undici"; + +import { describe, expect, test, vi } from "vitest"; + +import { fetchWithTrustedCertificatesAsync, mergeHeadersWithUserAgent } from "@homarr/core/infrastructure/http"; + +vi.mock("@homarr/core/infrastructure/certificates", () => ({ + getAllTrustedCertificatesAsync: vi.fn(async () => []), + getTrustedCertificateHostnamesAsync: vi.fn(async () => []), +})); describe("mergeHeadersWithUserAgent", () => { test("should keep existing headers when given a Headers instance", () => { @@ -48,3 +60,42 @@ describe("mergeHeadersWithUserAgent", () => { expect(merged.get("user-agent")).toContain("Homarr/"); }); }); + +const startCaptureServerAsync = async (): Promise<{ + server: Server; + getReceivedHeaders: () => Record; + port: number; +}> => { + let receivedHeaders: Record = {}; + const server = createServer((req, res) => { + receivedHeaders = req.headers; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end("{}"); + }); + await new Promise((resolve) => server.listen(0, resolve)); + const port = (server.address() as AddressInfo).port; + return { + server, + port, + getReceivedHeaders: () => receivedHeaders, + }; +}; + +describe("fetchWithTrustedCertificatesAsync", () => { + test("should deliver a Headers instance with all headers to the remote server", async () => { + // Arrange - the @immich/sdk hands over a Headers instance after merging its defaults + const { server, port, getReceivedHeaders } = await startCaptureServerAsync(); + const headers = new Headers({ "x-api-key": "immich-api-key", Accept: "application/json" }); + + // Act + const response = await fetchWithTrustedCertificatesAsync(`http://127.0.0.1:${port}/api/server/statistics`, { + headers: headers as unknown as RequestInit["headers"], + }); + server.close(); + + // Assert + expect(response.status).toBe(200); + expect(getReceivedHeaders()["x-api-key"]).toBe("immich-api-key"); + expect(getReceivedHeaders()["user-agent"]).toContain("Homarr/"); + }); +}); diff --git a/packages/integrations/test/immich-sdk-e2e.spec.ts b/packages/integrations/test/immich-sdk-e2e.spec.ts new file mode 100644 index 000000000..00072e91f --- /dev/null +++ b/packages/integrations/test/immich-sdk-e2e.spec.ts @@ -0,0 +1,37 @@ +// @vitest-environment node + +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; + +import { getServerStatistics, init } from "@immich/sdk"; +import { describe, expect, test, vi } from "vitest"; + +import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http"; + +vi.mock("@homarr/core/infrastructure/certificates", () => ({ + getAllTrustedCertificatesAsync: vi.fn(async () => []), + getTrustedCertificateHostnamesAsync: vi.fn(async () => []), +})); + +describe("real @immich/sdk end-to-end through fetchWithTrustedCertificatesAsync", () => { + test("x-api-key set via init() reaches the server", async () => { + let receivedXApiKey: string | string[] | undefined; + const server = createServer((req, res) => { + receivedXApiKey = req.headers["x-api-key"]; + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ usage: 1000, photos: 5, videos: 2 })); + }); + await new Promise((resolve) => server.listen(0, resolve)); + const port = (server.address() as AddressInfo).port; + + init({ baseUrl: `http://127.0.0.1:${port}/api`, apiKey: "sdk-configured-key" }); + const stats = await getServerStatistics({ + fetch: fetchWithTrustedCertificatesAsync as unknown as typeof fetch, + }); + + server.close(); + + expect(receivedXApiKey).toBe("sdk-configured-key"); + expect(stats).toMatchObject({ usage: 1000, photos: 5, videos: 2 }); + }); +});