diff --git a/tests/collect-launch-metrics.test.mjs b/tests/collect-launch-metrics.test.mjs index f4bf34ba..132003b1 100644 --- a/tests/collect-launch-metrics.test.mjs +++ b/tests/collect-launch-metrics.test.mjs @@ -43,7 +43,7 @@ function routes({ ]; } -function collect(routeList) { +function collect(routeList, env = {}) { return runScriptInSandbox({ script: 'collect-launch-metrics.mjs', // The script writes straight into data/, which only exists in the sandbox @@ -51,6 +51,7 @@ function collect(routeList) { fixtures: { 'data/.keep': '' }, routes: routeList, outputs: [OUTPUT], + env, }); } @@ -367,3 +368,36 @@ test('an unparseable Link rel="next" target is refused', () => { /Ignoring unparseable Link rel="next" target: http:\/\/\[v1\.unparseable/, ); }); + +test('paginated collection requests carry the token when GH_TOKEN is set', () => { + const result = collect(routes(), { GH_TOKEN: 'fixture-token' }); + + readBaseline(result); + const paginated = result.requests.filter( + (request) => + request.url.includes(CONTRIBUTORS_ROUTE) || + request.url.includes(ISSUES_ROUTE), + ); + assert.ok(paginated.length > 0, 'expected paginated collection requests'); + for (const request of paginated) { + assert.ok( + String(request.headers.Authorization).includes('fixture-token'), + `missing credential on ${request.url}`, + ); + } +}); + +test('paginated collection requests are unauthenticated without GH_TOKEN', () => { + const result = collect(routes()); + + readBaseline(result); + const paginated = result.requests.filter( + (request) => + request.url.includes(CONTRIBUTORS_ROUTE) || + request.url.includes(ISSUES_ROUTE), + ); + assert.ok(paginated.length > 0, 'expected paginated collection requests'); + for (const request of paginated) { + assert.equal('Authorization' in request.headers, false, request.url); + } +}); diff --git a/tests/collect-metrics.test.mjs b/tests/collect-metrics.test.mjs index 928d352d..1fcd9ade 100644 --- a/tests/collect-metrics.test.mjs +++ b/tests/collect-metrics.test.mjs @@ -505,3 +505,56 @@ test('collect-metrics refuses an unparseable Link rel=next target', () => { /Ignoring unparseable Link rel="next" target: http:\/\/\[v1\.unparseable/, ); }); + +test('collect-metrics tolerates a landscape.yml with no landscape key', () => { + const data = metricsFrom(run({ landscape: 'other: {}\n' })); + + const byId = Object.fromEntries( + data.metrics.map((entry) => [entry.id, entry]), + ); + assert.equal(byId['cncf-projects'].value, 0); + assert.deepEqual(data.breakdowns.projectCategories.values, []); + assert.deepEqual(data.breakdowns.projectMaturity.values, []); +}); + +test('collect-metrics skips a subcategory that declares no items', () => { + const data = metricsFrom( + run({ + landscape: `landscape: + - name: Orchestration + subcategories: + - name: Empty subcategory + - name: Scheduling + items: + - name: Kubernetes + project: graduated +`, + }), + ); + + const byId = Object.fromEntries( + data.metrics.map((entry) => [entry.id, entry]), + ); + assert.equal(byId['cncf-projects'].value, 1); + assert.deepEqual(data.breakdowns.projectCategories.values, [ + { name: 'Orchestration', value: 1 }, + ]); +}); + +test('collect-metrics files projects from an unnamed category under "Other"', () => { + const data = metricsFrom( + run({ + landscape: `landscape: + - subcategories: + - name: Scheduling + items: + - name: Kubernetes + project: graduated +`, + }), + ); + + assert.deepEqual(data.breakdowns.projectCategories.values, [ + { name: 'Other', value: 1 }, + ]); +}); diff --git a/tests/collect-radar-reports.test.mjs b/tests/collect-radar-reports.test.mjs index 90df5706..b6e240d6 100644 --- a/tests/collect-radar-reports.test.mjs +++ b/tests/collect-radar-reports.test.mjs @@ -337,3 +337,20 @@ test('fails the run when the first page errors', () => { ); assert.equal(badRequest.outputs[OUTPUT], null); }); + +test('an existing catalog without a radarReports key is treated as empty', () => { + const result = collect({ + routes: [ + { match: POSTS_ROUTE, body: [post()] }, + { match: TYPES_ROUTE, body: [radarTypeTerm()] }, + ], + fixtures: { + [OUTPUT]: JSON.stringify({ source: 'cncf.io reports' }, null, 2) + '\n', + }, + }); + + const catalog = readCatalog(result); + assert.equal(catalog.radarReports.length, 1); + assert.equal(catalog.radarReports[0].slug, 'radar-one'); + assert.equal(catalog.radarReports[0].summary, PLACEHOLDER_SUMMARY); +}); diff --git a/tests/helpers-script-sandbox.mjs b/tests/helpers-script-sandbox.mjs index 12059b38..9f7844f0 100644 --- a/tests/helpers-script-sandbox.mjs +++ b/tests/helpers-script-sandbox.mjs @@ -19,10 +19,18 @@ const repoRoot = new URL('..', import.meta.url).pathname; // before the script under test is evaluated, so network-dependent scripts run // offline and deterministically. Routes match by substring against the URL and // are consulted in declaration order, so more specific routes come first. +// Every call is appended to ENDUSERS_FETCH_LOG as one JSON object per line so +// the caller can assert on what the script actually sent, not only on what it +// did with the reply. const FETCH_STUB = ` +import { appendFileSync } from 'node:fs'; const routes = JSON.parse(process.env.ENDUSERS_FETCH_ROUTES ?? '[]'); -globalThis.fetch = async (input) => { +globalThis.fetch = async (input, init) => { const url = typeof input === 'string' ? input : input.url; + appendFileSync( + process.env.ENDUSERS_FETCH_LOG, + JSON.stringify({ url, headers: init?.headers ?? {} }) + '\\n', + ); const route = routes.find((candidate) => url.includes(candidate.match)); if (!route) throw new Error('unstubbed request: ' + url); if (route.networkError) throw new Error(route.networkError); @@ -111,7 +119,8 @@ exit 0 * @param {string[]} [options.outputs] repo-relative paths to read back afterwards * @param {Record} [options.env] extra environment variables * @returns {{status: number, stdout: string, stderr: string, - * outputs: Record}} + * outputs: Record, + * requests: Array<{url: string, headers: Record}>}} */ export function runScriptInSandbox({ script, @@ -154,6 +163,9 @@ export function runScriptInSandbox({ const stubPath = join(work, 'fetch-stub.mjs'); writeFileSync(stubPath, FETCH_STUB); + const fetchLog = join(work, 'fetch-log.jsonl'); + writeFileSync(fetchLog, ''); + const result = spawnSync( 'node', ['--import', stubPath, join(work, 'scripts', script)], @@ -167,6 +179,7 @@ export function runScriptInSandbox({ // whether or not the developer or CI job exports a token. GH_TOKEN: '', ENDUSERS_FETCH_ROUTES: JSON.stringify(routes), + ENDUSERS_FETCH_LOG: fetchLog, ENDUSERS_GIT_FIXTURES: fixtureRepos, ENDUSERS_REAL_GIT: realGitPath(), ...repoDates, @@ -188,6 +201,10 @@ export function runScriptInSandbox({ stdout: result.stdout ?? '', stderr: result.stderr ?? '', outputs: collected, + requests: readFileSync(fetchLog, 'utf8') + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line)), }; } finally { rmSync(work, { recursive: true, force: true });