0d3cb498a3
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Waiting to run
Test and Publish Multi-arch Docker Image / test (push) Waiting to run
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Blocked by required conditions
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Blocked by required conditions
Validate Renovate Config / Validate Renovate Configuration (push) Waiting to run
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
222 lines
7.7 KiB
TypeScript
222 lines
7.7 KiB
TypeScript
/**
|
|
* Configuration Loader Tests
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
import { loadConfig, loadConfigOrDefault } from '../../../src/codeScan/config/loader';
|
|
import { CodeScanSeverity, ConfigLoadError } from '../../../src/types/codeScan';
|
|
|
|
describe('Configuration Loader', () => {
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
// Create a temporary directory for test files
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-scan-test-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Clean up temporary directory
|
|
if (fs.existsSync(tempDir)) {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('loadConfig', () => {
|
|
it('should load valid configuration file', () => {
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(configPath, 'minimumSeverity: high\ndiffsOnly: false');
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config).toEqual({
|
|
minimumSeverity: CodeScanSeverity.HIGH,
|
|
diffsOnly: false,
|
|
});
|
|
});
|
|
|
|
it('should apply defaults for missing optional fields', () => {
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(configPath, '{}'); // Empty object
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config).toEqual({
|
|
minimumSeverity: CodeScanSeverity.MEDIUM,
|
|
diffsOnly: false,
|
|
});
|
|
});
|
|
|
|
it('should accept all valid severity levels', () => {
|
|
const testCases: Array<[CodeScanSeverity, string]> = [
|
|
[CodeScanSeverity.LOW, 'low'],
|
|
[CodeScanSeverity.MEDIUM, 'medium'],
|
|
[CodeScanSeverity.HIGH, 'high'],
|
|
[CodeScanSeverity.CRITICAL, 'critical'],
|
|
];
|
|
|
|
for (const [expectedLevel, levelString] of testCases) {
|
|
const configPath = path.join(tempDir, `config-${levelString}.yaml`);
|
|
fs.writeFileSync(configPath, `minimumSeverity: ${levelString}\ndiffsOnly: true`);
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config.minimumSeverity).toBe(expectedLevel);
|
|
expect(config.diffsOnly).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should throw ConfigLoadError if file does not exist', () => {
|
|
const nonExistentPath = path.join(tempDir, 'does-not-exist.yaml');
|
|
|
|
expect(() => loadConfig(nonExistentPath)).toThrow(ConfigLoadError);
|
|
expect(() => loadConfig(nonExistentPath)).toThrow('Configuration file not found');
|
|
});
|
|
|
|
it('should throw ConfigLoadError if file cannot be parsed', () => {
|
|
const configPath = path.join(tempDir, 'invalid.yaml');
|
|
fs.writeFileSync(configPath, '{ invalid yaml content [[[');
|
|
|
|
expect(() => loadConfig(configPath)).toThrow(ConfigLoadError);
|
|
expect(() => loadConfig(configPath)).toThrow('Failed to parse YAML');
|
|
});
|
|
|
|
it('should throw ConfigLoadError if validation fails', () => {
|
|
const configPath = path.join(tempDir, 'invalid-schema.yaml');
|
|
fs.writeFileSync(configPath, 'minimumSeverity: invalid-level\ndiffsOnly: not-a-boolean');
|
|
|
|
expect(() => loadConfig(configPath)).toThrow(ConfigLoadError);
|
|
expect(() => loadConfig(configPath)).toThrow('Invalid configuration');
|
|
});
|
|
|
|
it('should handle boolean diffsOnly values', () => {
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(configPath, 'minimumSeverity: medium\ndiffsOnly: true');
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config.diffsOnly).toBe(true);
|
|
});
|
|
|
|
it('should accept minSeverity as an alias for minimumSeverity', () => {
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(configPath, 'minSeverity: critical\ndiffsOnly: true');
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config.minimumSeverity).toBe(CodeScanSeverity.CRITICAL);
|
|
expect(config.diffsOnly).toBe(true);
|
|
});
|
|
|
|
it('should prefer minSeverity over minimumSeverity when both provided', () => {
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(configPath, 'minSeverity: low\nminimumSeverity: high\ndiffsOnly: false');
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config.minimumSeverity).toBe(CodeScanSeverity.LOW);
|
|
expect(config.diffsOnly).toBe(false);
|
|
});
|
|
|
|
it('should accept all valid severity levels with minSeverity alias', () => {
|
|
const testCases: Array<[CodeScanSeverity, string]> = [
|
|
[CodeScanSeverity.LOW, 'low'],
|
|
[CodeScanSeverity.MEDIUM, 'medium'],
|
|
[CodeScanSeverity.HIGH, 'high'],
|
|
[CodeScanSeverity.CRITICAL, 'critical'],
|
|
];
|
|
|
|
for (const [expectedLevel, levelString] of testCases) {
|
|
const configPath = path.join(tempDir, `config-min-${levelString}.yaml`);
|
|
fs.writeFileSync(configPath, `minSeverity: ${levelString}\ndiffsOnly: true`);
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config.minimumSeverity).toBe(expectedLevel);
|
|
expect(config.diffsOnly).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should accept inline guidance text', () => {
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(
|
|
configPath,
|
|
'minimumSeverity: high\nguidance: "Focus on authentication vulnerabilities"',
|
|
);
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config.guidance).toBe('Focus on authentication vulnerabilities');
|
|
});
|
|
|
|
it('should read guidanceFile and populate guidance field', () => {
|
|
const guidanceFilePath = path.join(tempDir, 'guidance.txt');
|
|
fs.writeFileSync(guidanceFilePath, 'Focus on authentication vulnerabilities');
|
|
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(configPath, `minimumSeverity: high\nguidanceFile: ${guidanceFilePath}`);
|
|
|
|
const config = loadConfig(configPath);
|
|
|
|
expect(config.guidance).toBe('Focus on authentication vulnerabilities');
|
|
});
|
|
|
|
it('should throw ConfigLoadError when guidanceFile does not exist', () => {
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(configPath, 'minimumSeverity: high\nguidanceFile: ./nonexistent.txt');
|
|
|
|
expect(() => loadConfig(configPath)).toThrow(ConfigLoadError);
|
|
expect(() => loadConfig(configPath)).toThrow(/Failed to read guidance file/);
|
|
});
|
|
|
|
it('should throw ConfigLoadError when both guidance and guidanceFile are specified', () => {
|
|
const guidanceFilePath = path.join(tempDir, 'guidance.txt');
|
|
fs.writeFileSync(guidanceFilePath, 'File guidance');
|
|
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(
|
|
configPath,
|
|
`minimumSeverity: high\nguidance: "Inline guidance"\nguidanceFile: ${guidanceFilePath}`,
|
|
);
|
|
|
|
expect(() => loadConfig(configPath)).toThrow(ConfigLoadError);
|
|
expect(() => loadConfig(configPath)).toThrow(/Cannot specify both guidance and guidanceFile/);
|
|
});
|
|
});
|
|
|
|
describe('loadConfigOrDefault', () => {
|
|
it('should return default config when no path provided', () => {
|
|
const config = loadConfigOrDefault();
|
|
|
|
expect(config).toEqual({
|
|
minimumSeverity: CodeScanSeverity.MEDIUM,
|
|
diffsOnly: false,
|
|
});
|
|
});
|
|
|
|
it('should load config when path provided', () => {
|
|
const configPath = path.join(tempDir, 'config.yaml');
|
|
fs.writeFileSync(configPath, 'minimumSeverity: critical\ndiffsOnly: true');
|
|
|
|
const config = loadConfigOrDefault(configPath);
|
|
|
|
expect(config).toEqual({
|
|
minimumSeverity: CodeScanSeverity.CRITICAL,
|
|
diffsOnly: true,
|
|
});
|
|
});
|
|
|
|
it('should return default config when empty string provided', () => {
|
|
const config = loadConfigOrDefault('');
|
|
|
|
expect(config).toEqual({
|
|
minimumSeverity: CodeScanSeverity.MEDIUM,
|
|
diffsOnly: false,
|
|
});
|
|
});
|
|
});
|
|
});
|