auto-claude: subtask-5-2 - Write unit tests for feed reader

Added comprehensive unit tests for feed-reader module covering:
- Package specifier parsing (parseAffectedSpecifier)
- Feed payload validation (isValidFeedPayload)
- Ed25519 signature verification (verifySignedPayload)
- Checksum URL generation (defaultChecksumsUrl)
- Local feed loading with signature/checksum verification
- Security validation and error handling

Enhanced test harness with crypto utilities:
- generateEd25519KeyPair() for test key generation
- signPayload() for creating test signatures

All 24 tests passing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
David Abutbul
2026-02-27 21:27:03 +02:00
parent 41db282c1e
commit cd7bdd95a0
2 changed files with 803 additions and 0 deletions
+779
View File
@@ -0,0 +1,779 @@
#!/usr/bin/env node
/**
* Feed reader tests for clawsec-analyst.
*
* Tests cover:
* - Package specifier parsing
* - Feed payload validation
* - Signature verification (Ed25519)
* - Checksum URL generation
* - Local feed loading with signature/checksum verification
* - Security domain validation
*
* Run: node skills/clawsec-analyst/test/feed-reader.test.mjs
*/
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
pass,
fail,
report,
exitWithResults,
generateEd25519KeyPair,
signPayload,
createTempDir,
} from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB_PATH = path.resolve(__dirname, "..", "lib");
// Dynamic import to ensure we test the actual module
const {
parseAffectedSpecifier,
isValidFeedPayload,
verifySignedPayload,
defaultChecksumsUrl,
loadLocalFeed,
loadRemoteFeed,
} = await import(`${LIB_PATH}/feed-reader.js`);
let tempDirCleanup;
// -----------------------------------------------------------------------------
// Helper functions
// -----------------------------------------------------------------------------
function createValidFeed() {
return JSON.stringify(
{
version: "1.0.0",
updated: "2026-02-08T12:00:00Z",
advisories: [
{
id: "TEST-001",
severity: "high",
affected: ["test-skill@1.0.0"],
},
],
},
null,
2,
);
}
function createChecksumManifest(files) {
const checksums = {};
for (const [name, content] of Object.entries(files)) {
checksums[name] = crypto.createHash("sha256").update(content).digest("hex");
}
return JSON.stringify(
{
schema_version: "1.0",
algorithm: "sha256",
files: checksums,
},
null,
2,
);
}
// -----------------------------------------------------------------------------
// Test: parseAffectedSpecifier - valid specifier with version
// -----------------------------------------------------------------------------
async function testParseAffectedSpecifier_WithVersion() {
const testName = "parseAffectedSpecifier: parses package@version correctly";
try {
const result = parseAffectedSpecifier("test-package@1.2.3");
if (result.name === "test-package" && result.versionSpec === "1.2.3") {
pass(testName);
} else {
fail(testName, `Expected {name: 'test-package', versionSpec: '1.2.3'}, got ${JSON.stringify(result)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: parseAffectedSpecifier - package without version
// -----------------------------------------------------------------------------
async function testParseAffectedSpecifier_WithoutVersion() {
const testName = "parseAffectedSpecifier: defaults to * when no version";
try {
const result = parseAffectedSpecifier("test-package");
if (result.name === "test-package" && result.versionSpec === "*") {
pass(testName);
} else {
fail(testName, `Expected {name: 'test-package', versionSpec: '*'}, got ${JSON.stringify(result)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: parseAffectedSpecifier - scoped package
// -----------------------------------------------------------------------------
async function testParseAffectedSpecifier_ScopedPackage() {
const testName = "parseAffectedSpecifier: handles scoped packages";
try {
const result = parseAffectedSpecifier("@scope/package@2.0.0");
if (result.name === "@scope/package" && result.versionSpec === "2.0.0") {
pass(testName);
} else {
fail(testName, `Expected {name: '@scope/package', versionSpec: '2.0.0'}, got ${JSON.stringify(result)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: parseAffectedSpecifier - empty string
// -----------------------------------------------------------------------------
async function testParseAffectedSpecifier_EmptyString() {
const testName = "parseAffectedSpecifier: returns null for empty string";
try {
const result = parseAffectedSpecifier("");
if (result === null) {
pass(testName);
} else {
fail(testName, `Expected null for empty string, got ${JSON.stringify(result)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: parseAffectedSpecifier - version range
// -----------------------------------------------------------------------------
async function testParseAffectedSpecifier_VersionRange() {
const testName = "parseAffectedSpecifier: handles version ranges";
try {
const result = parseAffectedSpecifier("package@>=1.0.0");
if (result.name === "package" && result.versionSpec === ">=1.0.0") {
pass(testName);
} else {
fail(testName, `Expected {name: 'package', versionSpec: '>=1.0.0'}, got ${JSON.stringify(result)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - valid payload
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_Valid() {
const testName = "isValidFeedPayload: accepts valid feed structure";
try {
const payload = {
version: "1.0.0",
advisories: [
{
id: "TEST-001",
severity: "high",
affected: ["package@1.0.0"],
},
],
};
const result = isValidFeedPayload(payload);
if (result === true) {
pass(testName);
} else {
fail(testName, "Expected true for valid payload");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - missing version
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_MissingVersion() {
const testName = "isValidFeedPayload: rejects payload missing version";
try {
const payload = {
advisories: [],
};
const result = isValidFeedPayload(payload);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for payload missing version");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - invalid advisory structure
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_InvalidAdvisory() {
const testName = "isValidFeedPayload: rejects invalid advisory structure";
try {
const payload = {
version: "1.0.0",
advisories: [
{
id: "TEST-001",
// missing severity and affected
},
],
};
const result = isValidFeedPayload(payload);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for invalid advisory structure");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - empty advisories array
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_EmptyAdvisories() {
const testName = "isValidFeedPayload: accepts empty advisories array";
try {
const payload = {
version: "1.0.0",
advisories: [],
};
const result = isValidFeedPayload(payload);
if (result === true) {
pass(testName);
} else {
fail(testName, "Expected true for empty advisories array");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - non-object
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_NonObject() {
const testName = "isValidFeedPayload: rejects non-object values";
try {
const result1 = isValidFeedPayload(null);
const result2 = isValidFeedPayload("string");
const result3 = isValidFeedPayload(123);
if (result1 === false && result2 === false && result3 === false) {
pass(testName);
} else {
fail(testName, "Expected false for all non-object values");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - valid signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_ValidSignature() {
const testName = "verifySignedPayload: accepts valid signature";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const signature = signPayload(payload, privateKeyPem);
const result = verifySignedPayload(payload, signature, publicKeyPem);
if (result === true) {
pass(testName);
} else {
fail(testName, "Expected true for valid signature");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - invalid signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_InvalidSignature() {
const testName = "verifySignedPayload: rejects tampered payload";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const signature = signPayload(payload, privateKeyPem);
// Tamper with payload
const tamperedPayload = "TAMPERED payload content";
const result = verifySignedPayload(tamperedPayload, signature, publicKeyPem);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for tampered payload");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - wrong key
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_WrongKey() {
const testName = "verifySignedPayload: rejects wrong public key";
try {
const keyPair1 = generateEd25519KeyPair();
const keyPair2 = generateEd25519KeyPair();
const payload = "test payload content";
const signature = signPayload(payload, keyPair1.privateKeyPem);
// Verify with different public key
const result = verifySignedPayload(payload, signature, keyPair2.publicKeyPem);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for wrong public key");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - malformed signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_MalformedSignature() {
const testName = "verifySignedPayload: rejects malformed signature";
try {
const { publicKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const result = verifySignedPayload(payload, "not-valid-base64!!!", publicKeyPem);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for malformed signature");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - empty signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_EmptySignature() {
const testName = "verifySignedPayload: rejects empty signature";
try {
const { publicKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const result = verifySignedPayload(payload, "", publicKeyPem);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for empty signature");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - JSON-wrapped signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_JsonWrappedSignature() {
const testName = "verifySignedPayload: accepts JSON-wrapped signature";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const signatureBase64 = signPayload(payload, privateKeyPem);
const jsonWrapped = JSON.stringify({ signature: signatureBase64 });
const result = verifySignedPayload(payload, jsonWrapped, publicKeyPem);
if (result === true) {
pass(testName);
} else {
fail(testName, "Expected true for JSON-wrapped signature");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: defaultChecksumsUrl - standard URL
// -----------------------------------------------------------------------------
async function testDefaultChecksumsUrl_StandardUrl() {
const testName = "defaultChecksumsUrl: generates correct checksums URL";
try {
const feedUrl = "https://example.com/advisories/feed.json";
const result = defaultChecksumsUrl(feedUrl);
if (result === "https://example.com/advisories/checksums.json") {
pass(testName);
} else {
fail(testName, `Expected 'https://example.com/advisories/checksums.json', got '${result}'`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: defaultChecksumsUrl - root URL
// -----------------------------------------------------------------------------
async function testDefaultChecksumsUrl_RootUrl() {
const testName = "defaultChecksumsUrl: handles root URL";
try {
const feedUrl = "https://example.com/feed.json";
const result = defaultChecksumsUrl(feedUrl);
if (result === "https://example.com/checksums.json") {
pass(testName);
} else {
fail(testName, `Expected 'https://example.com/checksums.json', got '${result}'`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - valid signed feed
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_ValidSigned() {
const testName = "loadLocalFeed: loads valid signed feed";
try {
const { path: tmpDir, cleanup } = await createTempDir();
tempDirCleanup = cleanup;
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const signature = signPayload(feedContent, privateKeyPem);
const feedPath = path.join(tmpDir, "feed.json");
const signaturePath = path.join(tmpDir, "feed.json.sig");
await fs.writeFile(feedPath, feedContent, "utf8");
await fs.writeFile(signaturePath, signature, "utf8");
const result = await loadLocalFeed(feedPath, {
publicKeyPem,
verifyChecksumManifest: false,
});
if (
result.version === "1.0.0" &&
result.advisories.length === 1 &&
result.advisories[0].id === "TEST-001"
) {
pass(testName);
} else {
fail(testName, `Unexpected feed payload: ${JSON.stringify(result)}`);
}
await cleanup();
tempDirCleanup = null;
} catch (error) {
fail(testName, error);
if (tempDirCleanup) await tempDirCleanup();
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - invalid signature
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_InvalidSignature() {
const testName = "loadLocalFeed: rejects invalid signature";
try {
const { path: tmpDir, cleanup } = await createTempDir();
tempDirCleanup = cleanup;
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const signature = signPayload(feedContent, privateKeyPem);
const feedPath = path.join(tmpDir, "feed.json");
const signaturePath = path.join(tmpDir, "feed.json.sig");
// Tamper with feed content after signing
const tamperedFeed = feedContent.replace("TEST-001", "TAMPERED-001");
await fs.writeFile(feedPath, tamperedFeed, "utf8");
await fs.writeFile(signaturePath, signature, "utf8");
try {
await loadLocalFeed(feedPath, {
publicKeyPem,
verifyChecksumManifest: false,
});
fail(testName, "Expected error for invalid signature");
} catch (error) {
if (error.message.includes("signature verification failed")) {
pass(testName);
} else {
fail(testName, `Unexpected error: ${error.message}`);
}
}
await cleanup();
tempDirCleanup = null;
} catch (error) {
fail(testName, error);
if (tempDirCleanup) await tempDirCleanup();
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - unsigned allowed
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_UnsignedAllowed() {
const testName = "loadLocalFeed: allows unsigned feed when explicitly enabled";
try {
const { path: tmpDir, cleanup } = await createTempDir();
tempDirCleanup = cleanup;
const feedContent = createValidFeed();
const feedPath = path.join(tmpDir, "feed.json");
await fs.writeFile(feedPath, feedContent, "utf8");
const result = await loadLocalFeed(feedPath, {
allowUnsigned: true,
});
if (result.version === "1.0.0" && result.advisories.length === 1) {
pass(testName);
} else {
fail(testName, `Unexpected feed payload: ${JSON.stringify(result)}`);
}
await cleanup();
tempDirCleanup = null;
} catch (error) {
fail(testName, error);
if (tempDirCleanup) await tempDirCleanup();
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - with checksum verification
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_WithChecksumVerification() {
const testName = "loadLocalFeed: verifies checksums when enabled";
try {
const { path: tmpDir, cleanup } = await createTempDir();
tempDirCleanup = cleanup;
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const signature = signPayload(feedContent, privateKeyPem);
const checksumManifest = createChecksumManifest({
"feed.json": feedContent,
"feed.json.sig": signature,
});
const checksumSignature = signPayload(checksumManifest, privateKeyPem);
const feedPath = path.join(tmpDir, "feed.json");
const signaturePath = path.join(tmpDir, "feed.json.sig");
const checksumsPath = path.join(tmpDir, "checksums.json");
const checksumsSignaturePath = path.join(tmpDir, "checksums.json.sig");
await fs.writeFile(feedPath, feedContent, "utf8");
await fs.writeFile(signaturePath, signature, "utf8");
await fs.writeFile(checksumsPath, checksumManifest, "utf8");
await fs.writeFile(checksumsSignaturePath, checksumSignature, "utf8");
const result = await loadLocalFeed(feedPath, {
publicKeyPem,
verifyChecksumManifest: true,
});
if (result.version === "1.0.0" && result.advisories.length === 1) {
pass(testName);
} else {
fail(testName, `Unexpected feed payload: ${JSON.stringify(result)}`);
}
await cleanup();
tempDirCleanup = null;
} catch (error) {
fail(testName, error);
if (tempDirCleanup) await tempDirCleanup();
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - invalid feed format
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_InvalidFormat() {
const testName = "loadLocalFeed: rejects invalid feed format";
try {
const { path: tmpDir, cleanup } = await createTempDir();
tempDirCleanup = cleanup;
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const invalidFeed = JSON.stringify({ invalid: "structure" });
const signature = signPayload(invalidFeed, privateKeyPem);
const feedPath = path.join(tmpDir, "feed.json");
const signaturePath = path.join(tmpDir, "feed.json.sig");
await fs.writeFile(feedPath, invalidFeed, "utf8");
await fs.writeFile(signaturePath, signature, "utf8");
try {
await loadLocalFeed(feedPath, {
publicKeyPem,
verifyChecksumManifest: false,
});
fail(testName, "Expected error for invalid feed format");
} catch (error) {
if (error.message.includes("Invalid advisory feed format")) {
pass(testName);
} else {
fail(testName, `Unexpected error: ${error.message}`);
}
}
await cleanup();
tempDirCleanup = null;
} catch (error) {
fail(testName, error);
if (tempDirCleanup) await tempDirCleanup();
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - checksum mismatch
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_ChecksumMismatch() {
const testName = "loadLocalFeed: rejects checksum mismatch";
try {
const { path: tmpDir, cleanup } = await createTempDir();
tempDirCleanup = cleanup;
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const signature = signPayload(feedContent, privateKeyPem);
// Create checksum manifest with original content
const checksumManifest = createChecksumManifest({
"feed.json": feedContent,
"feed.json.sig": signature,
});
const checksumSignature = signPayload(checksumManifest, privateKeyPem);
// Write tampered feed content
const tamperedFeed = feedContent.replace("TEST-001", "TAMPERED-001");
const tamperedSignature = signPayload(tamperedFeed, privateKeyPem);
const feedPath = path.join(tmpDir, "feed.json");
const signaturePath = path.join(tmpDir, "feed.json.sig");
const checksumsPath = path.join(tmpDir, "checksums.json");
const checksumsSignaturePath = path.join(tmpDir, "checksums.json.sig");
await fs.writeFile(feedPath, tamperedFeed, "utf8");
await fs.writeFile(signaturePath, tamperedSignature, "utf8");
await fs.writeFile(checksumsPath, checksumManifest, "utf8");
await fs.writeFile(checksumsSignaturePath, checksumSignature, "utf8");
try {
await loadLocalFeed(feedPath, {
publicKeyPem,
verifyChecksumManifest: true,
});
fail(testName, "Expected error for checksum mismatch");
} catch (error) {
if (error.message.includes("Checksum mismatch")) {
pass(testName);
} else {
fail(testName, `Unexpected error: ${error.message}`);
}
}
await cleanup();
tempDirCleanup = null;
} catch (error) {
fail(testName, error);
if (tempDirCleanup) await tempDirCleanup();
}
}
// -----------------------------------------------------------------------------
// Run all tests
// -----------------------------------------------------------------------------
async function runAllTests() {
console.log("=== Feed Reader Tests ===\n");
// parseAffectedSpecifier tests
await testParseAffectedSpecifier_WithVersion();
await testParseAffectedSpecifier_WithoutVersion();
await testParseAffectedSpecifier_ScopedPackage();
await testParseAffectedSpecifier_EmptyString();
await testParseAffectedSpecifier_VersionRange();
// isValidFeedPayload tests
await testIsValidFeedPayload_Valid();
await testIsValidFeedPayload_MissingVersion();
await testIsValidFeedPayload_InvalidAdvisory();
await testIsValidFeedPayload_EmptyAdvisories();
await testIsValidFeedPayload_NonObject();
// verifySignedPayload tests
await testVerifySignedPayload_ValidSignature();
await testVerifySignedPayload_InvalidSignature();
await testVerifySignedPayload_WrongKey();
await testVerifySignedPayload_MalformedSignature();
await testVerifySignedPayload_EmptySignature();
await testVerifySignedPayload_JsonWrappedSignature();
// defaultChecksumsUrl tests
await testDefaultChecksumsUrl_StandardUrl();
await testDefaultChecksumsUrl_RootUrl();
// loadLocalFeed tests
await testLoadLocalFeed_ValidSigned();
await testLoadLocalFeed_InvalidSignature();
await testLoadLocalFeed_UnsignedAllowed();
await testLoadLocalFeed_WithChecksumVerification();
await testLoadLocalFeed_InvalidFormat();
await testLoadLocalFeed_ChecksumMismatch();
report();
exitWithResults();
}
// Run tests
runAllTests().catch((error) => {
console.error("Test runner failed:", error);
process.exit(1);
});
@@ -3,6 +3,7 @@
* Provides consistent test reporting and runner utilities.
*/
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -55,6 +56,29 @@ export function exitWithResults() {
}
}
/**
* Generates an Ed25519 keypair for test use.
* @returns {{publicKeyPem: string, privateKeyPem: string}}
*/
export function generateEd25519KeyPair() {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
return { publicKeyPem, privateKeyPem };
}
/**
* Signs a payload with an Ed25519 private key.
* @param {string} data - Data to sign
* @param {string} privateKeyPem - PEM-encoded private key
* @returns {string} Base64-encoded signature
*/
export function signPayload(data, privateKeyPem) {
const privateKey = crypto.createPrivateKey(privateKeyPem);
const signature = crypto.sign(null, Buffer.from(data, "utf8"), privateKey);
return signature.toString("base64");
}
/**
* Creates a temporary directory for test use.
* @returns {Promise<{path: string, cleanup: Function}>} Object with temp dir path and cleanup function