UNPKG

secret-protection-custom-pattern-automation

Version:

A Playwright-based tool to automate GitHub secret scanning custom pattern management.

385 lines (333 loc) 13.8 kB
import { Pattern, PatternFile, Config, loadPatternFile, buildUrl, comparePatterns } from './secret_protection.js'; import chalk from 'chalk'; import * as yaml from 'js-yaml'; import { promises as fs } from 'fs'; import * as path from 'path'; // Simple test framework (reusing from validator.test.ts) class SimpleTest { private testCount = 0; private passCount = 0; private failCount = 0; test(name: string, testFn: () => void): void { this.testCount++; try { testFn(); this.passCount++; console.log(chalk.green(`✓ ${name}`)); } catch (error) { this.failCount++; console.log(chalk.red(`✗ ${name}`)); console.log(chalk.red(` Error: ${error instanceof Error ? error.message : String(error)}`)); } } async testAsync(name: string, testFn: () => Promise<void>): Promise<void> { this.testCount++; try { await testFn(); this.passCount++; console.log(chalk.green(`✓ ${name}`)); } catch (error) { this.failCount++; console.log(chalk.red(`✗ ${name}`)); console.log(chalk.red(` Error: ${error instanceof Error ? error.message : String(error)}`)); } } assertEquals(actual: any, expected: any, message?: string): void { if (JSON.stringify(actual) !== JSON.stringify(expected)) { throw new Error(message || `Expected ${JSON.stringify(expected)}, but got ${JSON.stringify(actual)}`); } } assertTrue(condition: boolean, message?: string): void { if (!condition) { throw new Error(message || 'Expected condition to be true'); } } assertFalse(condition: boolean, message?: string): void { if (condition) { throw new Error(message || 'Expected condition to be false'); } } assertContainsString(actual: string, expected: string, message?: string): void { if (!actual.includes(expected)) { throw new Error(message || `Expected string to contain "${expected}", but got "${actual}"`); } } assertThrows(fn: () => void, expectedError?: string, message?: string): void { try { fn(); throw new Error(message || 'Expected function to throw an error'); } catch (error) { if (expectedError && !(error instanceof Error && error.message.includes(expectedError))) { throw new Error(message || `Expected error containing "${expectedError}", but got "${error}"`); } } } summary(): void { console.log(chalk.bold(`\n📊 Test Summary:`)); console.log(chalk.green(`✓ Passed: ${this.passCount}`)); console.log(chalk.red(`✗ Failed: ${this.failCount}`)); console.log(chalk.blue(`📝 Total: ${this.testCount}`)); if (this.failCount === 0) { console.log(chalk.green.bold('\n🎉 All tests passed!')); } else { console.log(chalk.red.bold('\n❌ Some tests failed!')); process.exit(1); } } } async function runMainFunctionalityTests() { console.log(chalk.bold.blue('\n🧪 Running Main Functionality Tests\n')); const test = new SimpleTest(); // Test buildUrl function test.test('buildUrl should build correct repo URLs', () => { const config: Config = { server: 'https://github.com', target: 'owner/repo', scope: 'repo', dryRunThreshold: 0, maxTestTries: 25 }; const url = buildUrl(config, 'settings', 'security_analysis'); test.assertEquals(url, 'https://github.com/owner/repo/settings/security_analysis'); }); test.test('buildUrl should build correct org URLs', () => { const config: Config = { server: 'https://github.com', target: 'myorg', scope: 'org', dryRunThreshold: 0, maxTestTries: 25 }; const url = buildUrl(config, 'settings', 'security_analysis'); test.assertEquals(url, 'https://github.com/organizations/myorg/settings/security_analysis'); }); test.test('buildUrl should build correct enterprise URLs', () => { const config: Config = { server: 'https://github.enterprise.com', target: 'myenterprise', scope: 'enterprise', dryRunThreshold: 0, maxTestTries: 25 }; const url = buildUrl(config, 'settings', 'security_analysis_policies', 'security_features'); test.assertEquals(url, 'https://github.enterprise.com/enterprises/myenterprise/settings/security_analysis_policies/security_features'); }); test.test('buildUrl should throw error for invalid repo format', () => { const config: Config = { server: 'https://github.com', target: 'invalid-repo-format', scope: 'repo', dryRunThreshold: 0, maxTestTries: 25 }; test.assertThrows( () => buildUrl(config, 'settings'), 'Invalid repository format' ); }); // Test comparePatterns function test.test('comparePatterns should return true for identical patterns', () => { test.assertTrue(comparePatterns('test-pattern', 'test-pattern')); }); test.test('comparePatterns should return true for patterns with different whitespace', () => { test.assertTrue(comparePatterns(' test-pattern ', 'test-pattern')); test.assertTrue(comparePatterns('test-pattern\n', ' test-pattern ')); }); test.test('comparePatterns should return false for different patterns', () => { test.assertFalse(comparePatterns('pattern1', 'pattern2')); }); test.test('comparePatterns should handle null/undefined values', () => { test.assertFalse(comparePatterns(null, 'pattern')); test.assertFalse(comparePatterns('pattern', undefined)); test.assertFalse(comparePatterns(null, undefined)); test.assertFalse(comparePatterns(undefined, null)); }); // Test loadPatternFile function await test.testAsync('loadPatternFile should load valid YAML files', async () => { const tempFile = path.join(process.cwd(), 'temp-test-pattern.yml'); const testPattern: PatternFile = { name: 'Test Patterns', patterns: [{ name: 'Test Pattern', regex: { version: 1, pattern: 'test.*pattern' } }] }; try { await fs.writeFile(tempFile, yaml.dump(testPattern)); const loaded = await loadPatternFile(tempFile); test.assertEquals(loaded.name, 'Test Patterns'); test.assertEquals(loaded.patterns.length, 1); test.assertEquals(loaded.patterns[0].name, 'Test Pattern'); test.assertEquals(loaded.patterns[0].regex.pattern, 'test.*pattern'); } finally { try { await fs.unlink(tempFile); } catch { // Ignore cleanup errors } } }); await test.testAsync('loadPatternFile should load valid JSON files', async () => { const tempFile = path.join(process.cwd(), 'temp-test-pattern.json'); const testPattern: PatternFile = { name: 'Test JSON Patterns', patterns: [{ name: 'JSON Test Pattern', regex: { version: 1, pattern: 'json.*pattern' } }] }; try { await fs.writeFile(tempFile, JSON.stringify(testPattern, null, 2)); const loaded = await loadPatternFile(tempFile); test.assertEquals(loaded.name, 'Test JSON Patterns'); test.assertEquals(loaded.patterns.length, 1); test.assertEquals(loaded.patterns[0].name, 'JSON Test Pattern'); } finally { try { await fs.unlink(tempFile); } catch { // Ignore cleanup errors } } }); await test.testAsync('loadPatternFile should throw error for invalid files', async () => { const tempFile = path.join(process.cwd(), 'temp-invalid-pattern.txt'); try { await fs.writeFile(tempFile, '{{{{'); let threwError = false; try { await loadPatternFile(tempFile); } catch { threwError = true; } test.assertTrue(threwError, 'Should have thrown an error for invalid file'); } finally { try { await fs.unlink(tempFile); } catch { // Ignore cleanup errors } } }); // Test pattern filtering logic (simulating the logic from uploadPatterns) test.test('pattern filtering should work with include patterns', () => { const patterns: Pattern[] = [ { name: 'Pattern A', regex: { version: 1, pattern: 'a' } }, { name: 'Pattern B', regex: { version: 1, pattern: 'b' } }, { name: 'Pattern C', regex: { version: 1, pattern: 'c' } } ]; const config: Config = { server: 'https://github.com', target: 'test/repo', scope: 'repo', patternsToInclude: ['Pattern A', 'Pattern C'], dryRunThreshold: 0, maxTestTries: 25 }; const filtered = patterns.filter(pattern => { if (config.patternsToInclude && !config.patternsToInclude.includes(pattern.name)) { return false; } if (config.patternsToExclude && config.patternsToExclude.includes(pattern.name)) { return false; } return true; }); test.assertEquals(filtered.length, 2); test.assertEquals(filtered[0].name, 'Pattern A'); test.assertEquals(filtered[1].name, 'Pattern C'); }); test.test('pattern filtering should work with exclude patterns', () => { const patterns: Pattern[] = [ { name: 'Pattern A', regex: { version: 1, pattern: 'a' } }, { name: 'Pattern B', regex: { version: 1, pattern: 'b' } }, { name: 'Pattern C', regex: { version: 1, pattern: 'c' } } ]; const config: Config = { server: 'https://github.com', target: 'test/repo', scope: 'repo', patternsToExclude: ['Pattern B'], dryRunThreshold: 0, maxTestTries: 25 }; const filtered = patterns.filter(pattern => { if (config.patternsToInclude && !config.patternsToInclude.includes(pattern.name)) { return false; } if (config.patternsToExclude && config.patternsToExclude.includes(pattern.name)) { return false; } return true; }); test.assertEquals(filtered.length, 2); test.assertEquals(filtered[0].name, 'Pattern A'); test.assertEquals(filtered[1].name, 'Pattern C'); }); // Test configuration validation logic test.test('config validation should detect conflicting push protection flags', () => { // Simulate the validation logic from parseArgs const testValidation = (enablePushProtection: boolean, noChangePushProtection: boolean, disablePushProtection: boolean) => { if (enablePushProtection && noChangePushProtection) { return 'Both --enable-push-protection and --no-change-push-protection are set'; } if (enablePushProtection && disablePushProtection) { return 'Both --enable-push-protection and --disable-push-protection are set'; } if (disablePushProtection && noChangePushProtection) { return 'Both --disable-push-protection and --no-change-push-protection are set'; } return null; }; test.assertContainsString( testValidation(true, true, false) || '', 'enable-push-protection and --no-change-push-protection' ); test.assertContainsString( testValidation(true, false, true) || '', 'enable-push-protection and --disable-push-protection' ); test.assertContainsString( testValidation(false, true, true) || '', 'disable-push-protection and --no-change-push-protection' ); test.assertEquals(testValidation(true, false, false), null); test.assertEquals(testValidation(false, false, false), null); }); // Test scope detection logic test.test('scope detection should work correctly', () => { const detectScope = (target: string, argScope?: string) => { if (target.includes('/')) { return 'repo'; } else if (argScope === undefined) { return 'org'; } else { return argScope; } }; test.assertEquals(detectScope('owner/repo'), 'repo'); test.assertEquals(detectScope('myorg'), 'org'); test.assertEquals(detectScope('myenterprise', 'enterprise'), 'enterprise'); test.assertEquals(detectScope('myorg', 'org'), 'org'); }); test.summary(); } // Export for use in other test files or standalone execution export { runMainFunctionalityTests }; // Run tests if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { runMainFunctionalityTests(); }