Close XCA parity gaps: advanced extensions, CA/revocation UI, imports, ZIP export, E2E

This commit is contained in:
2026-10-23 04:26:35 +00:00
parent 4f3a632f4b
commit 5eb7866f5b
27 changed files with 2054 additions and 134 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
playwright-report/
test-results/
+29
View File
@@ -0,0 +1,29 @@
# KCA browser end-to-end tests
Playwright tests that exercise the core PKI workflow through the real SPA and
API.
Prerequisites:
```sh
# from the repository root
docker compose up --build
# add the mock IdP host entry once:
echo "127.0.0.1 mockoauth" | sudo tee -a /etc/hosts
cd e2e
npm install
npx playwright install chromium
npm test
```
The default base URL is `http://localhost:8080`. Override it with
`KCA_BASE_URL`, e.g. when testing the Trunk dev server on `:8000` or a
non-default Compose port:
```sh
KCA_BASE_URL=http://localhost:8083 npm test
```
The tests use unique usernames, so repeated runs do not collide. The mock OIDC
provider accepts any username.
+60
View File
@@ -0,0 +1,60 @@
{
"name": "kca-e2e",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kca-e2e",
"version": "0.1.0",
"devDependencies": {
"@playwright/test": "^1.64.0"
}
},
"node_modules/@playwright/test": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
"integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.64.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.64.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright-core": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "kca-e2e",
"private": true,
"version": "0.1.0",
"description": "Browser end-to-end tests for KCA",
"scripts": {
"test": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.64.0"
}
}
+22
View File
@@ -0,0 +1,22 @@
// Playwright configuration for the KCA browser suite.
//
// The default target is the Docker Compose stack on :8080. Point
// KCA_BASE_URL at another port when testing a local Trunk/Granian setup.
const { defineConfig } = require("@playwright/test");
const baseURL = process.env.KCA_BASE_URL || "http://localhost:8080";
module.exports = defineConfig({
testDir: "./tests",
timeout: 60_000,
expect: { timeout: 15_000 },
fullyParallel: false,
workers: 1,
reporter: [["list"]],
use: {
baseURL,
headless: true,
ignoreHTTPSErrors: true,
trace: "retain-on-failure",
},
});
+62
View File
@@ -0,0 +1,62 @@
// Advanced extensions, preview/validation and vault ZIP export.
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();
}
test("advanced extensions, validation and vault export", async ({ page }) => {
const user = unique("advanced");
const vaultName = unique("Advanced");
const certName = unique("advanced.example");
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();
await page.goto(`${vaultUrl}/certificates/new`);
await expect(page.getByRole("heading", { name: "New certificate" })).toBeVisible();
await (await field(page, "Internal name")).fill(certName);
await (await field(page, "Common name (CN)")).fill(certName);
await (await field(page, "Organization (O)")).fill("KCA E2E");
await (await field(page, "Country (C)")).fill("DE");
await page.locator("#new-key").check();
await page.locator("textarea[placeholder='http://example.com/ca.crl']").fill("http://example.com/ca.crl");
await page.locator("textarea[placeholder='http://ocsp.example.com']").fill("http://ocsp.example.com");
await page.locator("textarea[placeholder*='raw']").fill(
'{"raw":[{"oid":"1.2.3.4","critical":false,"der_hex":"0c03616263"}]}'
);
await page.getByRole("button", { name: "Validate extensions" }).click();
await expect(page.getByText("Extension preview")).toBeVisible();
await expect(page.getByText("CRL Distribution Points", { exact: false })).toBeVisible();
await expect(page.getByText("1.2.3.4", { exact: false })).toBeVisible();
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByRole("heading", { name: certName })).toBeVisible();
await expect(page.getByText("CRL Distribution Points", { exact: false })).toBeVisible();
await expect(page.getByText("1.2.3.4", { exact: false })).toBeVisible();
// ZIP export prompts for the private-key password.
await page.goto(vaultUrl);
page.once("dialog", (dialog) => dialog.accept("secret"));
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Export ZIP" }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.zip$/);
});
+96
View File
@@ -0,0 +1,96 @@
// End-to-end smoke test for the KCA PKI workflow.
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).toHaveURL(/\/$/);
await expect(page.getByRole("heading", { name: "Vaults" })).toBeVisible();
}
test("core PKI workflow", async ({ page }) => {
const user = unique("e2e");
const vaultName = unique("Vault");
const caName = unique("E2E CA");
const leafName = unique("www.e2e.example");
await login(page, user);
// Vault
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();
await expect(page.getByRole("heading", { name: vaultName })).toBeVisible();
const vaultUrl = page.url();
// Key
await page.getByRole("link", { name: "New key" }).click();
await page.getByPlaceholder("webserver-key").fill(`${caName} key`);
await page.getByRole("button", { name: "Generate key" }).click();
await expect(page.getByRole("heading", { name: `${caName} key` })).toBeVisible();
// CA certificate
await page.goto(`${vaultUrl}/certificates/new`);
await expect(page.getByRole("heading", { name: "New certificate" })).toBeVisible();
await (await field(page, "Internal name")).fill(caName);
await (await field(page, "Common name (CN)")).fill(caName);
await (await field(page, "Organization (O)")).fill("KCA E2E");
await (await field(page, "Country (C)")).fill("DE");
await page.locator("#new-key").check();
await (await field(page, "Basic constraints")).selectOption("ca");
await page.locator("label").filter({ hasText: "Certificate sign" }).locator("..").locator("input[type=checkbox]").check();
await page.locator("label").filter({ hasText: "CRL sign" }).locator("..").locator("input[type=checkbox]").check();
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByText("CA certificate", { exact: false })).toBeVisible();
const caUrl = page.url();
// Leaf signed by the CA
await page.goto(`${vaultUrl}/certificates/new`);
await (await field(page, "Internal name")).fill(leafName);
await (await field(page, "Issuer")).selectOption({ label: caName });
await (await field(page, "Common name (CN)")).fill(leafName);
await (await field(page, "Organization (O)")).fill("KCA E2E");
await (await field(page, "Country (C)")).fill("DE");
await page.locator("#new-key").check();
await page.locator("input[placeholder*='DNS:example.com']").fill(`DNS:${leafName}`);
await page.locator("label").filter({ hasText: "TLS server" }).locator("..").locator("input[type=checkbox]").check();
await page.getByRole("button", { name: "Create certificate" }).click();
await expect(page.getByRole("heading", { name: leafName })).toBeVisible();
await expect(page.getByText("serverAuth", { exact: false })).toBeVisible();
// Search
await page.goto(`${vaultUrl}/search`);
await page.locator("input[placeholder='Search…']").fill(leafName);
await page.getByRole("button", { name: "Search" }).click();
await expect(page.getByRole("link", { name: leafName, exact: true })).toBeVisible();
// Revoke
await page.getByRole("link", { name: leafName, exact: true }).click();
await page.getByRole("button", { name: "Revoke certificate" }).click();
await expect(page.getByText("revoked", { exact: true })).toBeVisible();
// CRL from the CA
await page.goto(caUrl);
await expect(page.getByRole("heading", { name: caName })).toBeVisible();
await page.getByRole("button", { name: "Generate CRL" }).click();
await expect(page).toHaveURL(/\/crls\//);
await expect(page.getByText(/CRL number \d+ · 1 revoked/)).toBeVisible();
// API token
await page.goto("/tokens");
await page.getByPlaceholder("CI uploader").fill(unique("token"));
await page.getByRole("button", { name: "Create" }).click();
await expect(page.getByText("Token created", { exact: false })).toBeVisible();
});