20260723 新增 rpki-explorer 单容器部署资产并修复 parsed 投影渲染(#128):deploy/rpki-explorer 全套独立 compose 部署资产(对接 231 在跑 soak、retain 10 runs、SSH 转发访问);ProjectionView 按真实 API 结构解包 projection.object、证书走 resourceCertificate+extensions、CRL 读顶层字段并新增 IP/AS 资源格式化;e2e fixtures 对齐真实嵌套结构并新增证书/CRL parsed 覆盖

This commit is contained in:
2026-07-23 17:59:11 +08:00
parent 3d772f748c
commit b5c033426e
11 changed files with 788 additions and 50 deletions

View File

@@ -209,7 +209,21 @@ export const OBJ_ROA_REJECTED = {
rejectReason: "manifest stale: thisUpdate in the past",
};
export const OBJECTS = [OBJ_ROA, OBJ_MFT, OBJ_CER_REJECTED, OBJ_ASPA, OBJ_ROA_REJECTED];
export const OBJ_CRL = {
objectInstanceId: "obj-crl-0001",
uri: "rsync://repo-a.example/ca1/main.crl",
sha256: SHA("f"),
objectType: "crl",
result: "ok",
detailSummary: null,
repoId: REPO_A.repoId,
ppId: PP_1.ppId,
sourceSection: "fresh",
rejected: false,
rejectReason: null,
};
export const OBJECTS = [OBJ_ROA, OBJ_MFT, OBJ_CER_REJECTED, OBJ_ASPA, OBJ_ROA_REJECTED, OBJ_CRL];
export const STATS_VALIDATION = { ok: 1279, error: 2, skipped: 3 };
export const STATS_OBJECT_TYPES = {
@@ -226,45 +240,65 @@ export const STATS_REASONS = {
};
export const ROA_PROJECTION = {
schemaVersion: 1,
schemaVersion: 2,
sha256: SHA("a"),
objectType: "roa",
parseStatus: "ok",
// Mirrors the live query service shape:
// projection = { input, object: { type, roa|manifest|… }, schemaVersion, tool }
projection: {
type: "roa",
roa: {
asId: 65001,
ipAddressFamilies: [
{
afi: "ipv4",
addresses: [
{ prefix: "203.0.113.0/24", maxLength: 24 },
{ prefix: "198.51.100.0/25", maxLength: 25 },
],
},
{
afi: "ipv6",
addresses: [{ prefix: "2001:db8::/32", maxLength: 48 }],
},
],
input: {
type: "roa",
path: "rsync://repo-a.example/ca1/alice.roa",
bytes: { len: 1280, sha256: SHA("a") },
},
object: {
type: "roa",
roa: {
asId: 65001,
ipAddressFamilies: [
{
afi: "ipv4",
addresses: [
{ prefix: "203.0.113.0/24", maxLength: 24 },
{ prefix: "198.51.100.0/25", maxLength: 25 },
],
},
{
afi: "ipv6",
addresses: [{ prefix: "2001:db8::/32", maxLength: 48 }],
},
],
},
},
schemaVersion: 2,
tool: "rpki",
},
};
export const MFT_PROJECTION = {
schemaVersion: 1,
schemaVersion: 2,
sha256: SHA("b"),
objectType: "mft",
parseStatus: "ok",
projection: {
type: "mft",
manifest: {
manifestNumberHex: "0A2F",
thisUpdate: "2026-07-16T23:00:00Z",
nextUpdate: "2026-07-17T23:00:00Z",
fileHashAlg: "sha256",
fileCount: 3,
input: {
type: "mft",
path: "rsync://repo-a.example/ca1/manifest.mft",
bytes: { len: 2048, sha256: SHA("b") },
},
object: {
type: "mft",
manifest: {
manifestNumberHex: "0A2F",
thisUpdate: "2026-07-16T23:00:00Z",
nextUpdate: "2026-07-17T23:00:00Z",
fileHashAlg: "sha256",
fileCount: 3,
},
},
schemaVersion: 2,
tool: "rpki",
},
};
@@ -274,6 +308,125 @@ export const MFT_FILES = [
{ fileName: "stale.cer", hashHex: SHA("c") },
];
// Certificate projection in the real nested API shape:
// certificate payload under `object.resourceCertificate`, SKI/AKI/SIA/AIA/CRLDP
// and IP/AS resources under `extensions`.
export const CER_PROJECTION = {
schemaVersion: 2,
sha256: SHA("c"),
objectType: "cer",
parseStatus: "ok",
projection: {
input: {
type: "cer",
path: "rsync://repo-a.example/ca1/stale.cer",
bytes: { len: 1534, sha256: SHA("c") },
},
object: {
type: "cer",
decode: { profileValid: true },
resourceCertificate: {
kind: "Ca",
version: 3,
serialNumberHex: "02C6C7",
issuer: "CN=A90DC5BE, serialNumber=0E65A4F5",
subject: "CN=A9114E750000, serialNumber=5A179648",
validity: { notBefore: "2026-07-23T00:30:06Z", notAfter: "2027-09-30T00:00:00Z" },
signatureAlgorithm: "1.2.840.113549.1.1.11",
extensions: {
subjectKeyIdentifier: "5a179648b3ef2369dce7bdb58140ff7dc7060abf",
authorityKeyIdentifier: "0e65a4f5fd36b5bd68eb3c923408978c907aa79f",
basicConstraintsCa: true,
subjectInfoAccess: {
accessDescriptions: [
{ accessMethodOid: "1.3.6.1.5.5.7.48.5", accessLocation: "rsync://repo-a.example/ca1/" },
],
},
caIssuersUris: ["rsync://repo-a.example/ca1/issuer.cer"],
crlDistributionPointsUris: ["rsync://repo-a.example/ca1/main.crl"],
ipResources: {
families: [
{
afi: "Ipv4",
choice: {
AddressesOrRanges: [
{ Prefix: { addr: [103, 152, 34, 0], afi: "Ipv4", prefix_len: 23 } },
{ Range: { min: { addr: [192, 0, 2, 0] }, max: { addr: [192, 0, 2, 255] } } },
],
},
},
{
afi: "Ipv6",
choice: {
AddressesOrRanges: [
{
Prefix: {
addr: [38, 6, 104, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
afi: "Ipv6",
prefix_len: 32,
},
},
],
},
},
],
},
asResources: {
asnum: {
AsIdsOrRanges: [{ Id: 38008 }, { Id: 38023 }, { Range: { min: 65536, max: 65551 } }],
},
},
},
},
},
schemaVersion: 2,
tool: "rpki",
},
};
// CRL projection in the real nested API shape: CRL fields sit directly on the
// typed payload (no `crl` wrapper); crlNumber/AKI live under `extensions`.
export const CRL_PROJECTION = {
schemaVersion: 2,
sha256: SHA("f"),
objectType: "crl",
parseStatus: "ok",
projection: {
input: {
type: "crl",
path: "rsync://repo-a.example/ca1/main.crl",
bytes: { len: 900, sha256: SHA("f") },
},
object: {
type: "crl",
decode: { profileValid: true },
issuer: "CN=A9114E750000, serialNumber=5A179648",
thisUpdate: "2026-07-23T02:47:00Z",
nextUpdate: "2026-07-24T02:52:00Z",
signatureAlgorithm: "1.2.840.113549.1.1.11",
extensions: {
authorityKeyIdentifier: "5a179648b3ef2369dce7bdb58140ff7dc7060abf",
crlNumber: 3394,
crlNumberHex: "0D42",
},
revokedCertificates: {
count: 2,
entries: [
{ serialNumberHex: "1B0B0AD86D78E77D1491B5069E6287D08EEA7C17", revocationDate: "2027-02-13T13:22:01Z" },
{ serialNumberHex: "6A106263F88133615B7E8D3466C192CEC0904F2D", revocationDate: "2027-03-18T19:58:00Z" },
],
},
},
schemaVersion: 2,
tool: "rpki",
},
};
export const CRL_REVOKED = [
{ serialNumberHex: "1B0B0AD86D78E77D1491B5069E6287D08EEA7C17", revocationDate: "2027-02-13T13:22:01Z" },
{ serialNumberHex: "6A106263F88133615B7E8D3466C192CEC0904F2D", revocationDate: "2027-03-18T19:58:00Z" },
];
export const VALIDATION_SUMMARY = {
finalStatus: "valid",
auditResult: "ok",

View File

@@ -5,7 +5,10 @@
*/
import type { Page, Route } from "@playwright/test";
import {
CER_PROJECTION,
CHAIN_EDGES,
CRL_PROJECTION,
CRL_REVOKED,
EXPLAIN_RECORD,
HEALTH,
MFT_FILES,
@@ -303,6 +306,8 @@ export async function installMockApi(
if (b === "parsed" && !c) {
if (obj.objectType === "roa") return fulfillJson(route, ROA_PROJECTION);
if (obj.objectType === "manifest") return fulfillJson(route, MFT_PROJECTION);
if (obj.objectType === "certificate") return fulfillJson(route, CER_PROJECTION);
if (obj.objectType === "crl") return fulfillJson(route, CRL_PROJECTION);
return fulfillJson(route, null);
}
if (b === "parsed" && c === "manifest-files") {
@@ -310,7 +315,8 @@ export async function installMockApi(
return fulfillJson(route, items, next);
}
if (b === "parsed" && c === "revoked-certs") {
return fulfillJson(route, [], null);
const { page: items, next } = paginate(obj.objectType === "crl" ? CRL_REVOKED : [], url, 100);
return fulfillJson(route, items, next);
}
if (b === "validation" && !c) return fulfillJson(route, VALIDATION_SUMMARY);
if (b === "validation" && c === "explain" && request.method() === "POST") {

View File

@@ -0,0 +1,37 @@
import { expect, test } from "@playwright/test";
import { installMockApi } from "./mockApi";
test.beforeEach(async ({ page }) => {
await installMockApi(page);
});
test("certificate object renders the resourceCertificate projection", async ({ page }) => {
await page.goto("/objects/obj-cer-0001");
await expect(page.getByText("Serial number")).toBeVisible();
await expect(page.getByText("02C6C7")).toBeVisible();
await expect(page.getByText("CN=A90DC5BE, serialNumber=0E65A4F5")).toBeVisible();
await expect(page.getByText("CN=A9114E750000, serialNumber=5A179648")).toBeVisible();
await expect(page.getByText("5a179648b3ef2369dce7bdb58140ff7dc7060abf").first()).toBeVisible();
await expect(page.getByText("rsync://repo-a.example/ca1/main.crl")).toBeVisible();
// IP resources rendered from the real serde shape (prefix + range, v4 + v6)
await expect(page.getByText("103.152.34.0/23")).toBeVisible();
await expect(page.getByText("192.0.2.0 - 192.0.2.255")).toBeVisible();
await expect(page.getByText("2606:6800::/32")).toBeVisible();
// AS resources rendered as AS chips (Id + Range)
await expect(page.getByText("AS38008", { exact: true })).toBeVisible();
await expect(page.getByText("AS65536-AS65551")).toBeVisible();
await expect(page.getByText("1.2.840.113549.1.1.11")).toBeVisible();
});
test("crl object renders header fields from the top-level payload", async ({ page }) => {
await page.goto("/objects/obj-crl-0001");
await expect(page.getByText("CN=A9114E750000, serialNumber=5A179648")).toBeVisible();
await expect(page.getByText("CRL number")).toBeVisible();
await expect(page.getByText("0D42")).toBeVisible();
await expect(page.getByText("5a179648b3ef2369dce7bdb58140ff7dc7060abf")).toBeVisible();
// Revoked certificates table from the sub-endpoint
await expect(page.getByRole("cell", { name: "1B0B0AD86D78E77D1491B5069E6287D08EEA7C17" })).toBeVisible();
await expect(page.getByRole("cell", { name: "6A106263F88133615B7E8D3466C192CEC0904F2D" })).toBeVisible();
});