Files
clawsec/scripts/test-github-traffic-archive.mjs
T
davida-ps 9fd3059271 fix(traffic): require a traffic-capable PAT for the archive workflow (#265)
* fix(traffic): use a traffic-capable PAT for the archive workflow

The daily Archive GitHub Traffic run has failed since creation: the
TRAFFIC_ARCHIVE_TOKEN secret was never provisioned, so the workflow fell
back to github.token, which GitHub categorically rejects on traffic
endpoints (403 "Resource not accessible by integration").

- Fall back to the existing POLL_NVD_CVES_PAT automation token instead
  of github.token, keeping TRAFFIC_ARCHIVE_TOKEN as the preferred
  override once provisioned.
- Fail fast with an actionable error when no traffic-capable token is
  configured.
- Explain token requirements in the script's 401/403 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(traffic): require dedicated TRAFFIC_ARCHIVE_TOKEN, drop expired PAT fallback

A live dispatch confirmed POLL_NVD_CVES_PAT is expired (401 Bad
credentials), so falling back to it only trades one daily failure for
another. Require the dedicated secret and fail fast with setup
instructions instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:25:56 +03:00

277 lines
9.3 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdtemp, readdir, readFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import {
buildTrafficSummary,
fetchGitHubTraffic,
mergeTrafficArchive,
writeJson,
} from './archive-github-traffic.mjs';
const TEST_REPOSITORY = 'prompt-security/clawsec';
const TEST_CAPTURE_DATE = Date.UTC(2026, 5, 3);
const utcDay = (offsetFromCaptureDate = 0) => {
const date = new Date(TEST_CAPTURE_DATE);
date.setUTCDate(date.getUTCDate() + offsetFromCaptureDate);
return `${date.toISOString().slice(0, 10)}T00:00:00Z`;
};
const captureAt = ({
offsetFromCaptureDate = 0,
hour = 3,
minute = 17,
} = {}) => {
const date = new Date(TEST_CAPTURE_DATE);
date.setUTCDate(date.getUTCDate() + offsetFromCaptureDate);
date.setUTCHours(hour, minute, 0, 0);
return date.toISOString();
};
const capturedAt = captureAt();
test('fetchGitHubTraffic requests the daily GitHub traffic endpoints with auth', async () => {
const calls = [];
const responses = {
[`/repos/${TEST_REPOSITORY}/traffic/views?per=day`]: {
count: 30,
uniques: 18,
views: [{ timestamp: utcDay(-1), count: 30, uniques: 18 }],
},
[`/repos/${TEST_REPOSITORY}/traffic/clones?per=day`]: {
count: 7,
uniques: 5,
clones: [{ timestamp: utcDay(-1), count: 7, uniques: 5 }],
},
[`/repos/${TEST_REPOSITORY}/traffic/popular/referrers`]: [
{ referrer: 'github.com', count: 12, uniques: 9 },
],
[`/repos/${TEST_REPOSITORY}/traffic/popular/paths`]: [
{ path: `/${TEST_REPOSITORY}`, title: TEST_REPOSITORY, count: 16, uniques: 10 },
],
};
const fetchImpl = async (url, options) => {
calls.push({ url: String(url), headers: options.headers });
const pathname = new URL(url).pathname;
const search = new URL(url).search;
const payload = responses[`${pathname}${search}`];
assert.ok(payload, `unexpected traffic endpoint: ${pathname}${search}`);
return new globalThis.Response(JSON.stringify(payload), { status: 200 });
};
const snapshot = await fetchGitHubTraffic({
repo: TEST_REPOSITORY,
token: 'test-token',
capturedAt,
fetchImpl,
});
assert.equal(calls.length, 4);
assert.ok(calls.every((call) => call.headers.Authorization === 'Bearer test-token'));
assert.deepEqual(snapshot.views.views, responses[`/repos/${TEST_REPOSITORY}/traffic/views?per=day`].views);
assert.deepEqual(snapshot.clones.clones, responses[`/repos/${TEST_REPOSITORY}/traffic/clones?per=day`].clones);
});
test('fetchGitHubTraffic explains traffic token requirements on 403', async () => {
const fetchImpl = async () => new globalThis.Response(
JSON.stringify({ message: 'Resource not accessible by integration' }),
{ status: 403 },
);
await assert.rejects(
fetchGitHubTraffic({
repo: TEST_REPOSITORY,
token: 'installation-token',
capturedAt,
fetchImpl,
}),
/returned 403\..*push access/,
);
});
test('fetchGitHubTraffic flags invalid tokens on 401', async () => {
const fetchImpl = async () => new globalThis.Response(
JSON.stringify({ message: 'Bad credentials' }),
{ status: 401 },
);
await assert.rejects(
fetchGitHubTraffic({
repo: TEST_REPOSITORY,
token: 'expired-token',
capturedAt,
fetchImpl,
}),
/returned 401\..*expired or revoked/,
);
});
test('mergeTrafficArchive upserts daily views and clones without double-counting overlapping windows', () => {
const archive = mergeTrafficArchive(
{
version: 1,
repository: TEST_REPOSITORY,
updated_at: captureAt({ offsetFromCaptureDate: -1 }),
daily: {
views: [
{ timestamp: utcDay(-2), count: 10, uniques: 6 },
{ timestamp: utcDay(-1), count: 20, uniques: 12 },
],
clones: [
{ timestamp: utcDay(-2), count: 2, uniques: 1 },
],
},
snapshots: {
referrers: [],
paths: [],
},
captures: [],
},
{
repository: TEST_REPOSITORY,
captured_at: capturedAt,
views: {
views: [
{ timestamp: utcDay(-1), count: 25, uniques: 14 },
{ timestamp: utcDay(), count: 35, uniques: 21 },
],
},
clones: {
clones: [
{ timestamp: utcDay(-1), count: 3, uniques: 2 },
{ timestamp: utcDay(), count: 5, uniques: 4 },
],
},
referrers: [{ referrer: 'github.com', count: 12, uniques: 9 }],
paths: [{ path: `/${TEST_REPOSITORY}`, title: TEST_REPOSITORY, count: 16, uniques: 10 }],
},
);
assert.deepEqual(archive.daily.views, [
{ timestamp: utcDay(-2), count: 10, uniques: 6 },
{ timestamp: utcDay(-1), count: 25, uniques: 14 },
{ timestamp: utcDay(), count: 35, uniques: 21 },
]);
assert.deepEqual(archive.daily.clones, [
{ timestamp: utcDay(-2), count: 2, uniques: 1 },
{ timestamp: utcDay(-1), count: 3, uniques: 2 },
{ timestamp: utcDay(), count: 5, uniques: 4 },
]);
});
test('mergeTrafficArchive keeps one referrer/path snapshot per capture date', () => {
const first = mergeTrafficArchive(undefined, {
repository: TEST_REPOSITORY,
captured_at: capturedAt,
views: { views: [] },
clones: { clones: [] },
referrers: [{ referrer: 'github.com', count: 12, uniques: 9 }],
paths: [{ path: `/${TEST_REPOSITORY}`, title: TEST_REPOSITORY, count: 16, uniques: 10 }],
});
const second = mergeTrafficArchive(first, {
repository: TEST_REPOSITORY,
captured_at: captureAt({ hour: 4, minute: 0 }),
views: { views: [] },
clones: { clones: [] },
referrers: [{ referrer: 'google.com', count: 8, uniques: 6 }],
paths: [{ path: `/${TEST_REPOSITORY}/wiki`, title: 'Wiki', count: 11, uniques: 7 }],
});
assert.equal(second.snapshots.referrers.length, 1);
assert.equal(second.snapshots.paths.length, 1);
assert.deepEqual(second.snapshots.referrers[0].entries, [
{ referrer: 'google.com', count: 8, uniques: 6 },
]);
assert.deepEqual(second.snapshots.paths[0].entries, [
{ path: `/${TEST_REPOSITORY}/wiki`, title: 'Wiki', count: 11, uniques: 7 },
]);
});
test('mergeTrafficArchive rejects blank referrer and path fields instead of archiving empty strings', () => {
assert.throws(
() => mergeTrafficArchive(undefined, {
repository: TEST_REPOSITORY,
captured_at: capturedAt,
views: { views: [] },
clones: { clones: [] },
referrers: [{ count: 12, uniques: 9 }],
paths: [],
}),
/referrers\.referrer must be a non-empty string/,
);
assert.throws(
() => mergeTrafficArchive(undefined, {
repository: TEST_REPOSITORY,
captured_at: capturedAt,
views: { views: [] },
clones: { clones: [] },
referrers: [],
paths: [{ path: `/${TEST_REPOSITORY}`, title: ' ', count: 16, uniques: 10 }],
}),
/paths\.title must be a non-empty string/,
);
});
test('writeJson replaces JSON through a same-directory temporary file', async () => {
const dir = await mkdtemp(path.join(os.tmpdir(), 'clawsec-traffic-json-'));
const file = path.join(dir, 'summary.json');
await writeJson(file, { version: 1, count: 1 });
await writeJson(file, { version: 1, count: 2 });
assert.equal(await readFile(file, 'utf8'), '{\n "version": 1,\n "count": 2\n}\n');
assert.deepEqual(await readdir(dir), ['summary.json']);
});
test('buildTrafficSummary reports count totals and labels summed daily uniques accurately', () => {
const archive = mergeTrafficArchive(undefined, {
repository: TEST_REPOSITORY,
captured_at: capturedAt,
views: {
views: [
{ timestamp: utcDay(-33), count: 100, uniques: 80 },
{ timestamp: utcDay(-1), count: 30, uniques: 18 },
{ timestamp: utcDay(), count: 40, uniques: 22 },
],
},
clones: {
clones: [
{ timestamp: utcDay(-1), count: 7, uniques: 5 },
{ timestamp: utcDay(), count: 9, uniques: 6 },
],
},
referrers: [],
paths: [],
});
const summary = buildTrafficSummary(archive, { now: captureAt({ hour: 12, minute: 0 }) });
assert.equal(summary.metrics.views.last_30_days.count, 70);
assert.equal(summary.metrics.views.last_30_days.sum_daily_uniques, 40);
assert.equal(summary.metrics.views.last_30_days.unique_semantics, 'sum_of_daily_uniques');
assert.equal(summary.metrics.views.all_time.count, 170);
assert.equal(summary.metrics.clones.last_30_days.count, 16);
assert.equal(summary.daily.views.length, 3);
});
test('traffic archive workflow uses a daily schedule and a dedicated archive branch', async () => {
const workflowPath = new URL('../.github/workflows/archive-traffic.yml', import.meta.url);
const workflow = await readFile(workflowPath, 'utf8');
assert.match(workflow, /cron:\s+'17 3 \* \* \*'/);
assert.match(workflow, /TRAFFIC_ARCHIVE_BRANCH:\s+traffic-archive/);
assert.match(workflow, /GH_TRAFFIC_TOKEN:\s*\$\{\{\s*secrets\.TRAFFIC_ARCHIVE_TOKEN\b/);
assert.doesNotMatch(workflow, /GH_TRAFFIC_TOKEN:[^\n]*github\.token/);
assert.match(workflow, /node scripts\/archive-github-traffic\.mjs/);
assert.match(workflow, /git add traffic\/archive\.json traffic\/summary\.json/);
assert.match(workflow, /git rm --ignore-unmatch traffic\/README\.md/);
assert.doesNotMatch(workflow, /git add .*traffic\/README\.md/);
assert.match(workflow, /git push origin HEAD:\$\{TRAFFIC_ARCHIVE_BRANCH\}/);
});