mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2026-11-15 14:38:04 +00:00
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:
4 files changed
+155
-4
No files matched your search
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user