Merge pull request #5939 from hafizahmedzia/fix/http-certificate-result-keys

fix: preserve hostname keys in HTTP certificate test results
This commit is contained in:
jc21 authored and GitHub committed 2026-11-05 10:05:11 +10:00
commit 5795699091
4 files changed
+155 -4

No files matched your search

+5 -4
View File
@@ -46,7 +46,7 @@ function buildBody(data?: Record<string, any>): string | undefined {
}
}
async function processResponse(response: Response) {
async function processResponse(response: Response, preserveResponseKeys = false) {
const payload = await response.json();
if (!response.ok) {
if (response.status === 401) {
@@ -59,7 +59,7 @@ async function processResponse(response: Response) {
typeof payload.error.messageI18n !== "undefined" ? payload.error.messageI18n : payload.error.message,
);
}
return camelizeKeys(payload) as any;
return (preserveResponseKeys ? payload : camelizeKeys(payload)) as any;
}
interface GetArgs {
@@ -97,9 +97,10 @@ interface PostArgs {
params?: queryString.StringifiableRecord;
data?: any;
noAuth?: boolean;
preserveResponseKeys?: boolean;
}
export async function post({ url, params, data, noAuth }: PostArgs, abortController?: AbortController) {
export async function post({ url, params, data, noAuth, preserveResponseKeys }: PostArgs, abortController?: AbortController) {
const apiUrl = buildUrl({ url, params });
const method = "POST";
@@ -126,7 +127,7 @@ export async function post({ url, params, data, noAuth }: PostArgs, abortControl
const signal = abortController?.signal;
const response = await fetch(apiUrl, { method, headers, body, signal });
return processResponse(response);
return processResponse(response, preserveResponseKeys);
}
interface PutArgs {
@@ -0,0 +1,92 @@
import AuthStore from "src/modules/AuthStore";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as api from "./base";
import { testHttpCertificate } from "./testHttpCertificate";
const fetchMock = vi.fn<typeof fetch>();
const reply = (payload: unknown, status = 200) => {
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify(payload), { status }));
};
beforeEach(() => {
localStorage.clear();
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
fetchMock.mockReset();
});
describe("certificate HTTP result keys", () => {
it.each([
"test-domain.example",
"test--domain.example",
"xn--bcher-kva.example",
"one.two-three.example",
"_acme-challenge.example",
"Test-Domain.example",
"plain.example",
])("preserves the exact response key %s", async (domain) => {
const result = { [domain]: "wrong-data" };
reply(result);
expect(await testHttpCertificate([domain])).toEqual(result);
});
it("keeps colliding spellings and their distinct statuses", async () => {
const result = { "test-domain.example": "ok", "test--domain.example": "no-host" };
reply(result);
expect(await testHttpCertificate(Object.keys(result))).toEqual(result);
});
it("sends unchanged domains with the normal request headers", async () => {
const domains = ["test-domain.example", "test--domain.example"];
reply({});
await testHttpCertificate(domains);
expect(fetchMock).toHaveBeenCalledWith("/api/nginx/certificates/test-http", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ domains }),
signal: undefined,
});
});
it("preserves an empty result", async () => {
reply({});
expect(await testHttpCertificate([])).toEqual({});
});
it.each([
[400, { message: "invalid domain" }, "invalid domain"],
[500, { message: "fallback", messageI18n: "certificate-error" }, "certificate-error"],
])("preserves error handling for status %s", async (status, error, message) => {
reply({ error }, status);
await expect(testHttpCertificate(["test-domain.example"])).rejects.toThrow(message);
});
it("still clears authentication on 401", async () => {
const clear = vi.spyOn(AuthStore, "clear");
const reload = vi.spyOn(window.location, "reload").mockImplementation(() => {});
reply({ error: { message: "unauthorized" } }, 401);
await expect(testHttpCertificate(["test-domain.example"])).rejects.toThrow("unauthorized");
expect(clear).toHaveBeenCalledOnce();
expect(reload).toHaveBeenCalledOnce();
});
it.each([
["get", api.get],
["post", api.post],
["put", api.put],
["del", api.del],
] as const)("keeps default %s response normalization", async (_name, method) => {
reply({ domain_names: ["test-domain.example"], nested_value: { created_on: "today" } });
expect(await method({ url: "/control" })).toEqual({
domainNames: ["test-domain.example"],
nestedValue: { createdOn: "today" },
});
});
it("preserves POST abort forwarding and request normalization", async () => {
const controller = new AbortController();
reply({ domain_names: [] });
await api.post({ url: "/control", data: { domainNames: ["a-b.example"] } }, controller);
expect(fetchMock).toHaveBeenCalledWith(
"/api/control",
expect.objectContaining({
body: JSON.stringify({ domain_names: ["a-b.example"] }),
signal: controller.signal,
}),
);
});
});
@@ -3,6 +3,7 @@ import * as api from "./base";
export async function testHttpCertificate(domains: string[]): Promise<Record<string, string>> {
return await api.post({
url: "/nginx/certificates/test-http",
preserveResponseKeys: true,
data: {
domains,
},
@@ -0,0 +1,57 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import EasyModal from "ez-modal-react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { showHTTPCertificateModal } from "./HTTPCertificateModal";
const fetchMock = vi.fn<typeof fetch>();
beforeEach(() => {
localStorage.clear();
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
fetchMock.mockReset();
});
async function openAndTest(domains: string[], payload: Record<string, string>) {
fetchMock.mockResolvedValueOnce(new Response(JSON.stringify(payload), { status: 200 }));
render(
<QueryClientProvider client={new QueryClient()}>
<EasyModal.Provider />
</QueryClientProvider>,
);
await act(async () => {
showHTTPCertificateModal();
});
const input = await screen.findByRole("combobox", { expanded: false });
for (const domain of domains) {
fireEvent.change(input, { target: { value: domain } });
fireEvent.keyDown(input, { key: "Enter", code: "Enter", keyCode: 13 });
await waitFor(() => expect(input).toHaveValue(""));
}
fireEvent.click(screen.getByRole("button", { name: "Test" }));
await waitFor(() => expect(fetchMock).toHaveBeenCalledOnce());
expect(JSON.parse(String(fetchMock.mock.calls[0][1]?.body))).toEqual({ domains });
}
describe("mounted HTTP certificate modal", () => {
it("renders a hyphenated result without changing its label", async () => {
await openAndTest(["test-domain.example"], { "test-domain.example": "wrong-data" });
expect(await screen.findByText("test-domain.example:")).toBeVisible();
expect(screen.queryByText("testDomain.example:")).not.toBeInTheDocument();
});
it("renders both collision-prone domains with distinct results", async () => {
await openAndTest(["test-domain.example", "test--domain.example"], {
"test-domain.example": "ok",
"test--domain.example": "no-host",
});
const first = await screen.findByText("test-domain.example:");
const second = await screen.findByText("test--domain.example:");
expect(
first.parentElement?.querySelector('[data-translation-id="certificates.http.reachability-ok"]'),
).not.toBeNull();
expect(
second.parentElement?.querySelector('[data-translation-id="certificates.http.reachability-not-resolved"]'),
).not.toBeNull();
});
});