20260728_2 修复rpki-explorer体验问题7项(#134):证书资源可展开、CMS对象新增EE Parsed Tab、Overview增加repo数KPI/修复Object types标签/Top repositories按objects排序(后端sort参数)/reject reasons按前缀聚类可下钻、Run选择器30s轮询
This commit is contained in:
@@ -84,7 +84,9 @@ export const REPO_C = {
|
||||
terminalStates: { fresh: 1 },
|
||||
};
|
||||
|
||||
export const REPOS = [REPO_A, REPO_B, REPO_C];
|
||||
// Deliberately not in object-count order: index order (300, 900, 84) differs
|
||||
// from `sort=objects&order=desc` (900, 300, 84) so tests can tell them apart.
|
||||
export const REPOS = [REPO_B, REPO_A, REPO_C];
|
||||
|
||||
export const PP_1 = {
|
||||
ppId: "node_1",
|
||||
@@ -237,6 +239,15 @@ export const STATS_OBJECT_TYPES = {
|
||||
export const STATS_REASONS = {
|
||||
"certificate expired: notAfter 2026-01-01T00:00:00Z": 1,
|
||||
"manifest stale: thisUpdate in the past": 1,
|
||||
"manifest is not valid at validation_time: this_update=2023-01-01T00:00:00Z": 3,
|
||||
"manifest is not valid at validation_time: this_update=2024-06-01T00:00:00Z": 2,
|
||||
};
|
||||
export const STATS_OVERVIEW = {
|
||||
publicationPoints: 4,
|
||||
objects: 1284,
|
||||
repos: REPOS.length,
|
||||
vrps: 987,
|
||||
aspas: 12,
|
||||
};
|
||||
|
||||
export const ROA_PROJECTION = {
|
||||
@@ -270,6 +281,43 @@ export const ROA_PROJECTION = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// CMS wrapper with the embedded EE certificate (real query service shape).
|
||||
signedObject: {
|
||||
signedData: {
|
||||
certificates: [
|
||||
{
|
||||
resourceCertificate: {
|
||||
kind: "Ee",
|
||||
version: 3,
|
||||
serialNumberHex: "0E65A501",
|
||||
issuer: "CN=A9114E750000, serialNumber=5A179648",
|
||||
subject: "CN=alice-roa-ee, serialNumber=0E65A501",
|
||||
validity: { notBefore: "2026-07-23T00:30:06Z", notAfter: "2027-01-01T00:00:00Z" },
|
||||
signatureAlgorithm: "1.2.840.113549.1.1.11",
|
||||
extensions: {
|
||||
subjectKeyIdentifier: "aa11bb22cc33dd44ee55ff66778899aabbccdd",
|
||||
authorityKeyIdentifier: "5a179648b3ef2369dce7bdb58140ff7dc7060abf",
|
||||
ipResources: {
|
||||
families: [
|
||||
{
|
||||
afi: "Ipv4",
|
||||
choice: {
|
||||
AddressesOrRanges: [
|
||||
{ Prefix: { addr: [203, 0, 113, 0], afi: "Ipv4", prefix_len: 24 } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
asResources: {
|
||||
asnum: { AsIdsOrRanges: [{ Id: 65001 }] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
schemaVersion: 2,
|
||||
tool: "rpki",
|
||||
@@ -373,7 +421,24 @@ export const CER_PROJECTION = {
|
||||
},
|
||||
asResources: {
|
||||
asnum: {
|
||||
AsIdsOrRanges: [{ Id: 38008 }, { Id: 38023 }, { Range: { min: 65536, max: 65551 } }],
|
||||
// 15 entries (> 12 collapsed limit) to exercise the "+N more" expand toggle.
|
||||
AsIdsOrRanges: [
|
||||
{ Id: 38008 },
|
||||
{ Id: 38023 },
|
||||
{ Range: { min: 65536, max: 65551 } },
|
||||
{ Id: 64496 },
|
||||
{ Id: 64497 },
|
||||
{ Id: 64498 },
|
||||
{ Id: 64499 },
|
||||
{ Id: 64500 },
|
||||
{ Id: 64501 },
|
||||
{ Id: 64502 },
|
||||
{ Id: 64503 },
|
||||
{ Id: 64504 },
|
||||
{ Id: 64505 },
|
||||
{ Id: 64506 },
|
||||
{ Id: 64507 },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
RUN_2,
|
||||
SERVICE_INFO,
|
||||
STATS_OBJECT_TYPES,
|
||||
STATS_OVERVIEW,
|
||||
STATS_REASONS,
|
||||
STATS_VALIDATION,
|
||||
VALIDATION_SUMMARY,
|
||||
@@ -234,8 +235,13 @@ export async function installMockApi(
|
||||
}
|
||||
|
||||
if (section === "repos" && !a) {
|
||||
const { page: items, next } = paginate(REPOS, url);
|
||||
return fulfillJson(route, items, next);
|
||||
let items = REPOS;
|
||||
if (url.searchParams.get("sort") === "objects") {
|
||||
const dir = url.searchParams.get("order") === "asc" ? 1 : -1;
|
||||
items = [...REPOS].sort((x, y) => dir * (x.objects - y.objects));
|
||||
}
|
||||
const { page: itemsPage, next } = paginate(items, url);
|
||||
return fulfillJson(route, itemsPage, next);
|
||||
}
|
||||
if (section === "repos" && a) {
|
||||
const repo = REPOS.find((r) => r.repoId === a);
|
||||
@@ -384,6 +390,7 @@ export async function installMockApi(
|
||||
if (a === "validation") return fulfillJson(route, STATS_VALIDATION);
|
||||
if (a === "object-types") return fulfillJson(route, STATS_OBJECT_TYPES);
|
||||
if (a === "reasons") return fulfillJson(route, STATS_REASONS);
|
||||
if (a === "overview") return fulfillJson(route, STATS_OVERVIEW);
|
||||
if (a === "downloads") return fulfillJson(route, { requests: 42, bytes: 1234567 });
|
||||
return fulfillJson(route, {});
|
||||
}
|
||||
|
||||
@@ -49,8 +49,35 @@ test("validation explain POSTs and renders the audit-projection notice", async (
|
||||
await expect(page.getByText("not a full revalidation")).toBeVisible();
|
||||
});
|
||||
|
||||
test("manifest object renders the file list", async ({ page }) => {
|
||||
await page.goto("/objects/obj-mft-0001");
|
||||
test("CMS-signed ROA shows the EE Parsed tab with the embedded certificate", async ({ page }) => {
|
||||
await page.goto("/objects/obj-roa-0001");
|
||||
|
||||
const eeTab = page.getByRole("tab", { name: "EE Parsed" });
|
||||
await expect(eeTab).toBeVisible();
|
||||
await eeTab.click();
|
||||
|
||||
// EE certificate fields rendered like the Certificate object Parsed view.
|
||||
await expect(page.getByText("Serial number")).toBeVisible();
|
||||
await expect(page.getByText("0E65A501").first()).toBeVisible();
|
||||
await expect(page.getByText("CN=alice-roa-ee, serialNumber=0E65A501")).toBeVisible();
|
||||
await expect(page.getByText("CN=A9114E750000, serialNumber=5A179648")).toBeVisible();
|
||||
await expect(page.getByText("aa11bb22cc33dd44ee55ff66778899aabbccdd")).toBeVisible();
|
||||
await expect(page.getByText("203.0.113.0/24")).toBeVisible();
|
||||
await expect(page.getByText("AS65001", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("1.2.840.113549.1.1.11")).toBeVisible();
|
||||
});
|
||||
|
||||
test("non-CMS objects do not show the EE Parsed tab", async ({ page }) => {
|
||||
await page.goto("/objects/obj-crl-0001");
|
||||
await expect(page.getByText("CRL number")).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "EE Parsed" })).not.toBeVisible();
|
||||
|
||||
await page.goto("/objects/obj-cer-0001");
|
||||
await expect(page.getByText("Serial number")).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "EE Parsed" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("manifest object renders the file list", async ({ page }) => { await page.goto("/objects/obj-mft-0001");
|
||||
await expect(page.getByText("Manifest number")).toBeVisible();
|
||||
await expect(page.getByText("0A2F")).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: "alice.roa" })).toBeVisible();
|
||||
|
||||
@@ -13,15 +13,28 @@ test("overview renders run strip, KPIs, charts, top repos and reasons", async ({
|
||||
await expect(page.getByText("VRPs")).toBeVisible();
|
||||
await expect(page.getByText("987")).toBeVisible();
|
||||
await expect(page.locator(".kpi-grid").getByText("Rejected")).toBeVisible();
|
||||
// Repositories KPI from stats/overview
|
||||
const reposKpi = page.locator(".kpi-grid > *", { hasText: "Repositories" });
|
||||
await expect(reposKpi).toBeVisible();
|
||||
await expect(reposKpi).toContainText("3");
|
||||
|
||||
// Charts render (recharts svg)
|
||||
await expect(page.locator(".recharts-responsive-container").first()).toBeVisible();
|
||||
|
||||
// Top repositories table
|
||||
await expect(page.getByRole("cell", { name: "repo-a.example" })).toBeVisible();
|
||||
// Top repositories table, sorted by objects desc (900 > 300 > 84),
|
||||
// not the fixture index order (300, 900, 84).
|
||||
const repoRows = page.locator(".overview-bottom table tbody tr");
|
||||
await expect(repoRows.first()).toContainText("repo-a.example");
|
||||
await expect(repoRows.nth(1)).toContainText("repo-b.example");
|
||||
await expect(repoRows.nth(2)).toContainText("repo-c.example");
|
||||
|
||||
// Top reject reasons
|
||||
await expect(page.getByText("certificate expired: notAfter")).toBeVisible();
|
||||
// Top reject reasons, clustered by prefix: the two validation_time
|
||||
// variants merge into one row with a combined count.
|
||||
const cluster = page.getByRole("link", { name: /manifest is not valid at validation_time/ });
|
||||
await expect(cluster).toBeVisible();
|
||||
await expect(cluster).toContainText("×2");
|
||||
await expect(cluster).toContainText("5");
|
||||
await expect(page.getByRole("link", { name: "certificate expired" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("overview does not request the global object list (first-screen discipline)", async ({
|
||||
@@ -59,5 +72,5 @@ test("overview drill-down: reason navigates to filtered validation page", async
|
||||
test("overview shows an explicit error state when the run cannot be loaded", async ({ page }) => {
|
||||
await installMockApi(page, { failLatestRun: true });
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("alert")).toContainText("Failed to load the latest run");
|
||||
await expect(page.getByRole("alert")).toContainText("Failed to load run latest");
|
||||
});
|
||||
|
||||
@@ -24,6 +24,25 @@ test("certificate object renders the resourceCertificate projection", async ({ p
|
||||
await expect(page.getByText("1.2.840.113549.1.1.11")).toBeVisible();
|
||||
});
|
||||
|
||||
test("certificate AS resources collapse behind an expandable '+N more' toggle", async ({ page }) => {
|
||||
await page.goto("/objects/obj-cer-0001");
|
||||
|
||||
// 15 AS entries: 12 visible, rest behind the toggle.
|
||||
const moreToggle = page.getByRole("button", { name: "+3 more" });
|
||||
await expect(moreToggle).toBeVisible();
|
||||
await expect(page.getByText("AS38008", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("AS64507", { exact: true })).not.toBeVisible();
|
||||
|
||||
await moreToggle.click();
|
||||
await expect(page.getByText("AS64507", { exact: true })).toBeVisible();
|
||||
await expect(page.getByText("AS64496", { exact: true })).toBeVisible();
|
||||
|
||||
const lessToggle = page.getByRole("button", { name: "Show less" });
|
||||
await expect(lessToggle).toBeVisible();
|
||||
await lessToggle.click();
|
||||
await expect(page.getByText("AS64507", { exact: true })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("crl object renders header fields from the top-level payload", async ({ page }) => {
|
||||
await page.goto("/objects/obj-crl-0001");
|
||||
|
||||
|
||||
@@ -61,6 +61,22 @@ test("run selector pins the run via ?run= and pages use it", async ({ page }) =>
|
||||
await expect.poll(() => requests.some((url) => url.includes("/runs/run_0001/"))).toBe(true);
|
||||
});
|
||||
|
||||
test("run selector polls for new runs without a page refresh", async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
let runsRequests = 0;
|
||||
page.on("request", (req) => {
|
||||
const url = new URL(req.url());
|
||||
if (/\/api\/v1\/runs$/.test(url.pathname)) runsRequests += 1;
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page.getByLabel("Run")).toBeVisible();
|
||||
// Initial fetch happens immediately; the 30s refetchInterval issues another.
|
||||
await expect
|
||||
.poll(() => runsRequests, { timeout: 45_000, intervals: [1_000, 2_000, 5_000] })
|
||||
.toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test("topbar search routes to the search page with the query", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const input = page.getByRole("searchbox", { name: "Global search" });
|
||||
@@ -87,11 +103,15 @@ test("deep links restore page state after reload", async ({ page }) => {
|
||||
|
||||
test("runs page lists runs, pins a run and expands artifacts", async ({ page }) => {
|
||||
await page.goto("/runs");
|
||||
await expect(page.getByRole("cell", { name: "run_0002" })).toBeVisible();
|
||||
await expect(page.getByRole("cell", { name: "run_0001" })).toBeVisible();
|
||||
// Scope to the row: the actions cell also contains the run id in its
|
||||
// accessible name ("Use run run_0002 …"), so a bare cell lookup is ambiguous.
|
||||
await expect(
|
||||
page.getByRole("row", { name: /run_0002/ }).getByRole("cell").first(),
|
||||
).toContainText("run_0002");
|
||||
const row = page.getByRole("row", { name: /run_0001/ });
|
||||
await expect(row.getByRole("cell").first()).toContainText("run_0001");
|
||||
|
||||
// Expand artifacts for run_0001
|
||||
const row = page.getByRole("row", { name: /run_0001/ });
|
||||
await row.getByRole("button", { name: "Artifacts" }).click();
|
||||
await expect(page.getByText("/data/runs/run_0001/report.json")).toBeVisible();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user