Implement setup command

This commit is contained in:
Sander Declerck 2025-09-05 15:54:50 +02:00
parent 59762d2472
commit 49ec685546
No known key found for this signature in database
3 changed files with 427 additions and 5 deletions

View file

@ -0,0 +1,299 @@
import { describe, it, before, after, mock } from "node:test";
import assert from "node:assert";
import fs from "fs";
import path from "path";
import os from "os";
import { addScannerToToml, setup } from "./setup.js";
describe("addScannerToToml", () => {
it("should add scanner to empty content", () => {
const result = addScannerToToml("");
assert.strictEqual(result.changed, true);
assert.strictEqual(
result.content.trim(),
`[install.security]\nscanner = "@aikidosec/safe-chain-bun"`
);
});
it("should add scanner to existing content without [install.security] section", () => {
const input = `[install]\nregistry = "https://registry.npmjs.org/"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes("[install.security]"));
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(result.content.includes('registry = "https://registry.npmjs.org/"'));
});
it("should add scanner to existing [install.security] section without scanner", () => {
const input = `[install.security]\n# Some comment`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes("[install.security]"));
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(result.content.includes("# Some comment"));
});
it("should replace existing scanner in [install.security] section", () => {
const input = `[install.security]\nscanner = "@other/scanner"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.strictEqual(
result.content.trim(),
`[install.security]\nscanner = "@aikidosec/safe-chain-bun"`
);
});
it("should not change content if safe-chain-bun scanner already configured", () => {
const input = `[install.security]\nscanner = "@aikidosec/safe-chain-bun"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, false);
assert.strictEqual(result.content, input);
});
it("should handle complex TOML with multiple sections", () => {
const input = `[install]
registry = "https://registry.npmjs.org/"
[test]
preload = ["./setup.ts"]
[install.security]
# Security configuration
scanner = "@other/old-scanner"
[build]
target = "node"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(!result.content.includes("@other/old-scanner"));
assert.ok(result.content.includes("registry = \"https://registry.npmjs.org/\""));
assert.ok(result.content.includes("[test]"));
assert.ok(result.content.includes("[build]"));
});
it("should handle scanner with different whitespace formatting", () => {
const input = `[install.security]\nscanner="@other/scanner"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
});
});
describe("setup function", () => {
let tempDir;
let originalConsoleLog;
let originalConsoleError;
let consoleOutput;
let consoleErrors;
before(() => {
// Create a temporary directory for test files
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "safe-chain-bun-test-"));
// Mock console output
consoleOutput = [];
consoleErrors = [];
originalConsoleLog = console.log;
originalConsoleError = console.error;
console.log = (...args) => consoleOutput.push(args.join(" "));
console.error = (...args) => consoleErrors.push(args.join(" "));
});
after(() => {
// Cleanup
console.log = originalConsoleLog;
console.error = originalConsoleError;
fs.rmSync(tempDir, { recursive: true, force: true });
});
// Helper to reset console mocks before each test
const resetConsole = () => {
consoleOutput.length = 0;
consoleErrors.length = 0;
};
it("should create new global config file", () => {
resetConsole();
const mockHomedir = mock.method(os, "homedir", () => tempDir);
const globalConfigPath = path.join(tempDir, ".bunfig.toml");
// Ensure file doesn't exist
if (fs.existsSync(globalConfigPath)) {
fs.unlinkSync(globalConfigPath);
}
setup();
assert.ok(fs.existsSync(globalConfigPath));
const content = fs.readFileSync(globalConfigPath, "utf8");
assert.ok(content.includes("[install.security]"));
assert.ok(content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(consoleOutput.some(msg => msg.includes("✅ Safe-Chain-Bun registered")));
mockHomedir.mock.restore();
});
it("should update existing global config file", () => {
resetConsole();
const mockHomedir = mock.method(os, "homedir", () => tempDir);
const globalConfigPath = path.join(tempDir, ".bunfig.toml");
// Create existing config
fs.writeFileSync(globalConfigPath, `[install]\nregistry = "https://registry.npmjs.org/"`);
setup();
const content = fs.readFileSync(globalConfigPath, "utf8");
assert.ok(content.includes("[install.security]"));
assert.ok(content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(content.includes('registry = "https://registry.npmjs.org/"'));
assert.ok(consoleOutput.some(msg => msg.includes("✅ Safe-Chain-Bun registered")));
mockHomedir.mock.restore();
});
it("should setup specific existing config file", () => {
resetConsole();
const specificConfigPath = path.join(tempDir, "project-bunfig.toml");
fs.writeFileSync(specificConfigPath, `[build]\ntarget = "node"`);
setup(specificConfigPath);
const content = fs.readFileSync(specificConfigPath, "utf8");
assert.ok(content.includes("[install.security]"));
assert.ok(content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(content.includes('target = "node"'));
assert.ok(consoleOutput.some(msg => msg.includes("✅ Safe-Chain-Bun registered")));
});
it("should report when already configured", () => {
resetConsole();
const mockHomedir = mock.method(os, "homedir", () => tempDir);
const globalConfigPath = path.join(tempDir, ".bunfig.toml");
// Create already configured file
fs.writeFileSync(globalConfigPath, `[install.security]\nscanner = "@aikidosec/safe-chain-bun"`);
setup();
assert.ok(consoleOutput.some(msg => msg.includes(" Safe-Chain-Bun is already configured")));
mockHomedir.mock.restore();
});
it("should fail when specific config file doesn't exist", () => {
resetConsole();
const nonExistentPath = path.join(tempDir, "does-not-exist.toml");
let exitCode;
const mockExit = mock.method(process, "exit", (code) => {
exitCode = code;
});
setup(nonExistentPath);
assert.strictEqual(exitCode, 1);
assert.ok(consoleErrors.some(msg => msg.includes("❌ Config file not found")));
mockExit.mock.restore();
});
it("should handle permission errors gracefully", () => {
resetConsole();
const mockHomedir = mock.method(os, "homedir", () => tempDir);
const globalConfigPath = path.join(tempDir, ".bunfig.toml");
// Create a directory where the file should be to cause EACCES error
if (fs.existsSync(globalConfigPath)) {
fs.unlinkSync(globalConfigPath);
}
fs.mkdirSync(globalConfigPath);
let exitCode;
const mockExit = mock.method(process, "exit", (code) => {
exitCode = code;
});
setup();
assert.strictEqual(exitCode, 1);
assert.ok(consoleErrors.some(msg => msg.includes("❌ Failed to setup Safe-Chain-Bun")));
// Cleanup
fs.rmSync(globalConfigPath, { recursive: true, force: true });
mockExit.mock.restore();
mockHomedir.mock.restore();
});
});
describe("Line endings compatibility", () => {
it("should handle Unix line endings (\\n)", () => {
const input = `[install]\nregistry = "https://registry.npmjs.org/"\n\n[build]\ntarget = "node"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes("[install.security]"));
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(result.content.includes("registry = \"https://registry.npmjs.org/\""));
});
it("should handle Windows line endings (\\r\\n)", () => {
const input = `[install]\r\nregistry = "https://registry.npmjs.org/"\r\n\r\n[build]\r\ntarget = "node"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes("[install.security]"));
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(result.content.includes("registry = \"https://registry.npmjs.org/\""));
});
it("should handle mixed line endings", () => {
const input = `[install]\r\nregistry = "https://registry.npmjs.org/"\n\n[build]\r\ntarget = "node"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes("[install.security]"));
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(result.content.includes("registry = \"https://registry.npmjs.org/\""));
});
it("should handle existing [install.security] with Windows line endings", () => {
const input = `[install.security]\r\nscanner = "@other/scanner"\r\n\r\n[build]\r\ntarget = "node"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
assert.ok(!result.content.includes("@other/scanner"));
});
it("should detect already configured scanner with Windows line endings", () => {
const input = `[install.security]\r\nscanner = "@aikidosec/safe-chain-bun"\r\n\r\n[build]\r\ntarget = "node"`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, false);
assert.strictEqual(result.content, input);
});
it("should normalize to system line endings like safe-chain", () => {
const input = `[build]\r\ntarget = "node"\r\n`;
const result = addScannerToToml(input);
assert.strictEqual(result.changed, true);
assert.ok(result.content.includes("[install.security]"));
assert.ok(result.content.includes('scanner = "@aikidosec/safe-chain-bun"'));
const lines = result.content.split(/\r?\n/);
const securitySectionIndex = lines.findIndex(line => line === "[install.security]");
assert.ok(securitySectionIndex >= 0, "Should find [install.security] section");
});
it("should properly detect patterns across different line endings", () => {
// Test regex patterns work correctly with different line endings
const windowsContent = `[install.security]\r\nscanner = "@aikidosec/safe-chain-bun"\r\n`;
const unixContent = `[install.security]\nscanner = "@aikidosec/safe-chain-bun"\n`;
const windowsResult = addScannerToToml(windowsContent);
const unixResult = addScannerToToml(unixContent);
// Both should detect as already configured
assert.strictEqual(windowsResult.changed, false);
assert.strictEqual(unixResult.changed, false);
});
});