diff --git a/tests/helpers-import-sandbox.mjs b/tests/helpers-import-sandbox.mjs index 670ed03b..b30b362f 100644 --- a/tests/helpers-import-sandbox.mjs +++ b/tests/helpers-import-sandbox.mjs @@ -88,6 +88,12 @@ export const FAKE_COMMIT = 'a'.repeat(40); * root and valued by link target. The `git clone` stub copies the fixture * with `cp -R`, which preserves symlinks, so these reach the importer the * same way a symlink committed upstream would. + * @param {string[]} [options.upstreamFifos] Named pipes created under the + * cloned upstream checkout, keyed by path relative to the clone root. A FIFO + * is neither a regular file nor a directory nor a symlink, which is the only + * way to reach walkFiles()' final rejection arm. `cp -R` recreates a FIFO as + * a FIFO, so these reach the importer the way a special file in the upstream + * checkout would. * @param {Record} [options.fetchResponses] * Stubbed responses keyed by absolute URL. * @param {Record} [options.repoFiles] Extra files placed in the @@ -102,6 +108,7 @@ export const FAKE_COMMIT = 'a'.repeat(40); export function runImportArchitectures({ upstream, upstreamSymlinks = {}, + upstreamFifos = [], fetchResponses = {}, repoFiles = {}, rsvgConvert = 'success', @@ -138,6 +145,16 @@ export function runImportArchitectures({ mkdirSync(dirname(link), { recursive: true }); symlinkSync(linkTarget, link); } + for (const relativePath of upstreamFifos) { + const fifo = join(upstreamFixture, relativePath); + mkdirSync(dirname(fifo), { recursive: true }); + // Node has no mkfifo binding, so shell out. Every platform the suite runs + // on ships mkfifo; a failure here is a broken fixture, not a skip. + const made = spawnSync('mkfifo', [fifo], { encoding: 'utf8' }); + if (made.status !== 0) { + throw new Error(`could not create FIFO ${relativePath}: ${made.stderr}`); + } + } for (const [relativePath, content] of Object.entries(repoFiles)) { writeAt(work, relativePath, content); } diff --git a/tests/import-architectures-fallbacks.test.mjs b/tests/import-architectures-fallbacks.test.mjs new file mode 100644 index 00000000..6bf2765a --- /dev/null +++ b/tests/import-architectures-fallbacks.test.mjs @@ -0,0 +1,255 @@ +// Region-coverage companion to tests/import-architectures.test.mjs. +// +// scripts/import-architectures.mjs is 100% line-covered, so every case here +// exercises a sub-line region that line coverage cannot see: a `??` fallback, +// the false arm of a ternary, or a `continue` guard whose line already ran. +// Three of them are fail-closed paths -- no hot-linking, no unmirrorable +// fetch, no special files -- where a regression would leave the line score +// untouched. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { runImportArchitectures } from './helpers-import-sandbox.mjs'; + +const architecture = (id, markdown, extras = {}) => ({ + [`content/en/architectures/${id}/index.md`]: markdown, + ...extras, +}); + +test('an empty frontmatter block falls back to an empty record, not a crash', () => { + const run = runImportArchitectures({ + upstream: architecture( + 'acme', + `--- +--- + +Acme runs a platform. +`, + ), + }); + + try { + assert.equal(run.status, 0, run.stderr); + const record = run.readJson('data/architectures/records/acme.json'); + // yamlParse('') is null, so `?? {}` is what keeps the property reads below + // from throwing; title and organization then fall back to the directory id. + assert.equal(record.title, 'acme'); + assert.equal(record.organization, 'acme'); + assert.deepEqual(record.industries, []); + assert.deepEqual(record.tags, []); + assert.equal(record.summary, 'Acme runs a platform.'); + } finally { + run.cleanup(); + } +}); + +test('a remote image outside cncf/artwork is demoted to a link, never hot-linked', () => { + const run = runImportArchitectures({ + upstream: architecture( + 'acme', + `--- +title: Acme +--- + +Acme runs a platform. + +![Topology diagram](https://cdn.example.com/diagram.png) +`, + ), + }); + + try { + assert.equal(run.status, 0, run.stderr); + const page = run.read('docs/architectures/acme.md'); + assert.match( + page, + /\[Topology diagram\]\(https:\/\/cdn\.example\.com\/diagram\.png\)/, + ); + // The image syntax must be gone: an `![...]` would make the browser fetch + // cdn.example.com on page load, which is the beacon this arm prevents. + assert.doesNotMatch(page, /!\[Topology diagram\]/); + } finally { + run.cleanup(); + } +}); + +test('a cncf/artwork image is rewritten to its mirrored local path', () => { + // Contrast case for the test above: the true arm of the same ternary. Without + // it, "no hot-link" would also be satisfied by dropping every image. + const url = + 'https://raw.githubusercontent.com/cncf/artwork/main/projects/kubernetes/icon/color/kubernetes-icon-color.png'; + const run = runImportArchitectures({ + upstream: architecture( + 'acme', + `--- +title: Acme +--- + +Acme runs a platform. + +![Kubernetes](${url}) +`, + ), + fetchResponses: { [url]: { status: 200, body: 'png-bytes' } }, + }); + + try { + assert.equal(run.status, 0, run.stderr); + const page = run.read('docs/architectures/acme.md'); + assert.match( + page, + /!\[Kubernetes\]\(\/img\/cncf-projects\/kubernetes-kubernetes-icon-color\.png\)/, + ); + assert.equal( + run.read('static/img/cncf-projects/kubernetes-kubernetes-icon-color.png'), + 'png-bytes', + ); + } finally { + run.cleanup(); + } +}); + +test('an artwork URL with no derivable mirror name is skipped before any fetch', () => { + // artworkPath() resolves this to `projects`, a single segment, so + // artworkMirrorPath() returns null and mirrorProjectAssets() must `continue` + // rather than fetch. The sandbox stubs fetch with an empty response table, + // so reaching the fetch would warn instead of silently succeeding. + const url = 'https://raw.githubusercontent.com/cncf/artwork/main/projects'; + const run = runImportArchitectures({ + upstream: architecture( + 'acme', + `--- +title: Acme +--- + +Acme runs a platform. + +![Artwork index](${url}) +`, + ), + fetchResponses: {}, + }); + + try { + assert.equal(run.status, 0, run.stderr); + assert.doesNotMatch(run.stderr, /Could not mirror CNCF project asset/); + assert.equal(run.exists('static/img/cncf-projects'), false); + // The unmirrorable URL also fails closed in the page: link, not image. + const page = run.read('docs/architectures/acme.md'); + assert.match(page, /\[Artwork index\]\(https:\/\/raw\.githubusercontent/); + assert.doesNotMatch(page, /!\[Artwork index\]/); + } finally { + run.cleanup(); + } +}); + +test('a body with no prose paragraph summarises to an empty string', () => { + const run = runImportArchitectures({ + upstream: architecture( + 'acme', + `--- +title: Acme +--- + +# Acme + +- Kubernetes +- Prometheus +`, + ), + }); + + try { + assert.equal(run.status, 0, run.stderr); + const record = run.readJson('data/architectures/records/acme.json'); + // Every paragraph starts with a structural character, so `find` returns + // undefined and the `?? ''` fallback supplies the summary. + assert.equal(record.summary, ''); + } finally { + run.cleanup(); + } +}); + +test('a special file in images/ is dropped by the walk, not reported as an asset', () => { + const run = runImportArchitectures({ + upstream: architecture( + 'acme', + `--- +title: Acme +--- + +Acme runs a platform. +`, + { 'content/en/architectures/acme/images/real.png': 'png-bytes' }, + ), + upstreamFifos: ['content/en/architectures/acme/images/pipe'], + }); + + try { + assert.equal(run.status, 0, run.stderr); + const record = run.readJson('data/architectures/records/acme.json'); + assert.deepEqual(record.assets, ['/img/architectures/acme/real.png']); + assert.equal(run.exists('static/img/architectures/acme/pipe'), false); + // walkFiles() drops the FIFO itself. Had it returned the path instead, the + // importer would have reached the extension check and warned about it -- + // and then tried to cpSync a named pipe. + assert.doesNotMatch(run.stderr, /is not a mirrorable image type/); + } finally { + run.cleanup(); + } +}); + +test('an SVG with no viewBox and no dimensions is left alone, not given a NaN viewBox', () => { + const svg = '\n'; + const run = runImportArchitectures({ + upstream: architecture( + 'acme', + `--- +title: Acme +--- + +Acme runs a platform. +`, + { 'content/en/architectures/acme/images/plain.svg': svg }, + ), + }); + + try { + assert.equal(run.status, 0, run.stderr); + const mirrored = run.read('static/img/architectures/acme/plain.svg'); + // Both dimension matches are null, so both ternaries take their NaN arm + // and the synthesised viewBox is skipped entirely. + assert.equal(mirrored, svg); + assert.doesNotMatch(mirrored, /viewBox/i); + assert.doesNotMatch(mirrored, /NaN/); + } finally { + run.cleanup(); + } +}); + +test('an SVG with explicit dimensions still gains a viewBox', () => { + // Contrast case for the test above: the ternaries' true arms must keep + // working, or "no NaN viewBox" would be satisfied by never adding one. + const svg = + '\n'; + const run = runImportArchitectures({ + upstream: architecture( + 'acme', + `--- +title: Acme +--- + +Acme runs a platform. +`, + { 'content/en/architectures/acme/images/sized.svg': svg }, + ), + }); + + try { + assert.equal(run.status, 0, run.stderr); + const mirrored = run.read('static/img/architectures/acme/sized.svg'); + assert.match(mirrored, /