Files
kca/e2e/tests/extension_editors.spec.js
T
Walter Oggioni 555f3f28f7
CI / Build and push docker image (push) Successful in 1m43s
CI / Build and push OCSP image (push) Successful in 33s
Combine AIA fields into XCA-style editor
2026-10-26 23:55:50 +08:00

189 lines
9.6 KiB
JavaScript

// XCA-style editors for advanced extension list fields.
const { test, expect } = require("@playwright/test");
function unique(prefix) {
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
}
async function field(page, label) {
return page.locator("label").filter({ hasText: label }).first().locator("..").locator("input, select, textarea").first();
}
async function login(page, username) {
await page.goto("/login");
await page.getByRole("link", { name: "Sign in with default" }).click();
await page.getByPlaceholder("Enter any user/subject").fill(username);
await page.getByRole("button", { name: "Sign-in" }).click();
await expect(page.getByRole("heading", { name: "Vaults" })).toBeVisible();
}
async function openEditor(page, buttonId, title) {
await page.locator(buttonId).click();
const dialog = page.getByRole("dialog", { name: title, exact: true });
await expect(dialog).toBeVisible();
return dialog;
}
async function addRow(dialog, type, value) {
const index = await dialog.locator(".list-entry").count();
await dialog.getByRole("button", { name: "Add", exact: true }).click();
const row = dialog.locator(".list-entry").nth(index);
await row.locator("select").selectOption(type);
await row.locator(".list-value").fill(value);
return row;
}
test("advanced extension list editors", async ({ page }) => {
const user = unique("list-editor");
const vaultName = unique("List Editors");
const certName = unique("constraints.example");
const crlUrls = ["https://ca.example.com/root.crl", "https://backup.example.com/root.crl"];
const aiaEntries = [
["OCSP;URI", "https://ocsp.example.com"],
["caIssuers;URI", "https://ca.example.com/issuer.crt"],
];
const aiaText = aiaEntries.map(([type, value]) => `${type}:${value}`).join(", ");
const cpsUrls = ["https://example.com/cps", "https://example.com/policy"];
await login(page, user);
await page.getByRole("button", { name: "New vault" }).click();
await page.getByPlaceholder("Production PKI").fill(vaultName);
await page.getByRole("button", { name: "Create vault" }).click();
await page.getByRole("link", { name: vaultName }).click();
const vaultUrl = page.url();
const settingsResponse = page.waitForResponse(
(response) => response.url().includes("/api/vaults/") && response.url().endsWith("/settings") && response.ok()
);
await page.goto(`${vaultUrl}/certificates/new`);
await settingsResponse;
await (await field(page, "Internal name")).fill(certName);
await (await field(page, "Common name (CN)")).fill(certName);
await (await field(page, "Basic constraints")).selectOption("ca");
await page.locator("#new-key").check();
// URI-list edits can be cancelled without changing the original textarea.
let dialog = await openEditor(page, "#edit-crl-urls", "CRL distribution point URLs");
await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked();
await addRow(dialog, "URI", "https://discarded.example.com/root.crl");
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
await expect(dialog).toHaveCount(0);
await expect(page.locator("#crl-urls")).toHaveValue("");
dialog = await openEditor(page, "#edit-crl-urls", "CRL distribution point URLs");
await expect(dialog.locator(".list-entry")).toHaveCount(0);
await addRow(dialog, "URI", crlUrls[0]);
await addRow(dialog, "URI", crlUrls[1]);
await dialog.getByLabel("Critical", { exact: true }).check();
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("#crl-urls")).toHaveValue(crlUrls.join("\n"));
dialog = await openEditor(page, "#edit-crl-urls", "CRL distribution point URLs");
await expect(dialog.locator(".list-entry")).toHaveCount(2);
await expect(dialog.locator(".list-entry").nth(1).locator(".list-value")).toHaveValue(crlUrls[1]);
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
// AIA combines the access method and XCA general-name type in each row.
dialog = await openEditor(page, "#edit-aia", "Authority information access");
await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked();
for (const [type, value] of aiaEntries) {
await addRow(dialog, type, value);
}
await dialog.getByLabel("Critical", { exact: true }).check();
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("#aia")).toHaveValue(aiaText);
dialog = await openEditor(page, "#edit-aia", "Authority information access");
await expect(dialog.locator(".list-entry")).toHaveCount(2);
await expect(dialog.locator(".list-entry").nth(0).locator("select")).toHaveValue("OCSP;URI");
await expect(dialog.locator(".list-entry").nth(1).locator("select")).toHaveValue("caIssuers;URI");
await expect(dialog.locator(".list-entry").nth(1).locator(".list-value")).toHaveValue(aiaEntries[1][1]);
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
// Name constraints use typed rows and share one extension-level critical flag.
dialog = await openEditor(page, "#edit-nc-permitted", "Name constraints · permitted");
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
await addRow(dialog, "DNS", ".example.com");
await addRow(dialog, "DNS", "example.net");
await dialog.getByLabel("Critical", { exact: true }).uncheck();
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("#nc-permitted")).toHaveValue("DNS:.example.com, DNS:example.net");
dialog = await openEditor(page, "#edit-nc-excluded", "Name constraints · excluded");
await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked();
await dialog.getByLabel("Critical", { exact: true }).check();
await addRow(dialog, "DNS", ".bad.example");
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("#nc-excluded")).toHaveValue("DNS:.bad.example");
dialog = await openEditor(page, "#edit-nc-permitted", "Name constraints · permitted");
await expect(dialog.locator(".list-entry")).toHaveCount(2);
await expect(dialog.getByLabel("Critical", { exact: true })).toBeChecked();
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
// CPS URIs use comma-separated plain URLs and support row deletion.
await (await field(page, "Certificate policy OIDs")).fill("1.3.6.1.4.1.1");
dialog = await openEditor(page, "#edit-policy-cps", "Policy CPS URIs");
await expect(dialog.getByLabel("Critical", { exact: true })).not.toBeChecked();
await dialog.getByLabel("Critical", { exact: true }).check();
await addRow(dialog, "URI", cpsUrls[0]);
await addRow(dialog, "URI", cpsUrls[1]);
const discarded = await addRow(dialog, "URI", "https://discarded.example.com/cps");
await discarded.getByRole("button", { name: "Delete", exact: true }).click();
await expect(dialog.locator(".list-entry")).toHaveCount(2);
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("#policy-cps")).toHaveValue(cpsUrls.join(", "));
await expect(await field(page, "Common name (CN)")).toHaveValue(certName);
await expect(page.locator("#nc-excluded")).toHaveValue("DNS:.bad.example");
await page.getByRole("button", { name: "Validate extensions" }).click();
await expect(page.getByText("Extension preview")).toBeVisible();
await expect(page.locator("pre.pem")).toContainText("Authority Information Access");
await expect(page.locator("pre.pem")).toContainText("Name Constraints");
await expect(page.locator("pre.pem")).toContainText("Certificate Policies");
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByRole("heading", { name: certName })).toBeVisible();
const certApiUrl = page.url().replace(/\/vaults\//, "/api/vaults/");
const detail = await page.evaluate(async (url) => {
const response = await fetch(url);
return response.json();
}, certApiUrl);
const extensions = detail.extension_spec;
expect(extensions.crl_distribution_points).toEqual({ critical: true, urls: crlUrls });
expect(extensions.authority_information_access).toEqual({
critical: true,
descriptions: aiaEntries.map(([type, value]) => ({
method: type.startsWith("OCSP") ? "ocsp" : "ca_issuers",
location: { type: "uri", value },
})),
});
expect(extensions.name_constraints).toEqual({
critical: true,
permitted: [
{ type: "dns", value: ".example.com" },
{ type: "dns", value: "example.net" },
],
excluded: [{ type: "dns", value: ".bad.example" }],
});
expect(extensions.certificate_policies).toEqual({
critical: true,
policies: [{ oid: "1.3.6.1.4.1.1", cps: cpsUrls }],
});
// The shared advanced-extension component also works in template forms.
await page.goto(`${vaultUrl}/templates/new`);
await expect(page.getByRole("heading", { name: "New template" })).toBeVisible();
dialog = await openEditor(page, "#edit-crl-urls", "CRL distribution point URLs");
await addRow(dialog, "URI", "https://template.example.com/root.crl");
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("#crl-urls")).toHaveValue("https://template.example.com/root.crl");
dialog = await openEditor(page, "#edit-aia", "Authority information access");
await addRow(dialog, "OCSP;URI", "https://template.example.com/ocsp");
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
await expect(page.locator("#aia")).toHaveValue("OCSP;URI:https://template.example.com/ocsp");
});