test(security): add comprehensive test suite for secret detection
This commit is contained in:
parent
64be499d96
commit
af64c12977
179
src/security/entropy.test.ts
Normal file
179
src/security/entropy.test.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
detectHighEntropyStrings,
|
||||
redactSecrets,
|
||||
} from './entropy.js';
|
||||
|
||||
describe('entropy detection', () => {
|
||||
describe('detectHighEntropyStrings', () => {
|
||||
it('should detect OpenAI API keys', () => {
|
||||
const text = 'My API key is sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets).toHaveLength(1);
|
||||
expect(result.secrets[0].type).toBe('api_key');
|
||||
expect(result.secrets[0].pattern).toContain('OpenAI');
|
||||
expect(result.secrets[0].confidence).toBe('high');
|
||||
});
|
||||
|
||||
it('should detect Anthropic API keys', () => {
|
||||
const text =
|
||||
'Use this: sk-ant-api03-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYz0123456789AbCdEfGhIjKlMnOpQrStUvWxYzAA';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets[0].type).toBe('api_key');
|
||||
expect(result.secrets[0].pattern).toContain('Anthropic');
|
||||
});
|
||||
|
||||
it('should detect GitHub tokens', () => {
|
||||
const text = 'Token: ghp_Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op5678Qr78Qr';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets[0].type).toBe('api_key');
|
||||
expect(result.secrets[0].pattern).toContain('GitHub');
|
||||
});
|
||||
|
||||
it('should detect Bearer tokens', () => {
|
||||
const text = 'Authorization: Bearer AbCdEfGhIjKlMnOpQrStUvWxYz0123456789';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets[0].type).toBe('bearer_token');
|
||||
expect(result.secrets[0].pattern).toContain('Bearer');
|
||||
});
|
||||
|
||||
it('should detect JWT tokens', () => {
|
||||
const text =
|
||||
'JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets[0].type).toBe('jwt');
|
||||
});
|
||||
|
||||
it('should detect private keys', () => {
|
||||
const text = 'Here is my key:\n-----BEGIN RSA PRIVATE KEY-----\nMIIE...';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets[0].type).toBe('private_key');
|
||||
});
|
||||
|
||||
it('should detect generic secret assignments', () => {
|
||||
const text = 'api_secret = "AbCdEfGhIjKlMnOpQrStUvWxYz0123"';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets[0].type).toBe('generic_secret');
|
||||
});
|
||||
|
||||
it('should detect high-entropy strings without pattern match', () => {
|
||||
const text = 'Random secret: Kj8mN2pQ5rT9vXwZ3bC7dFgH1iLaMnOqPs4tUy6';
|
||||
const result = detectHighEntropyStrings(text, { minEntropyThreshold: 4.0, minLength: 20 });
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should not flag UUIDs as secrets', () => {
|
||||
const text = 'Request ID: 550e8400-e29b-41d4-a716-446655440000';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(false);
|
||||
});
|
||||
|
||||
it('should not flag base64 images as secrets', () => {
|
||||
const text = 'Image: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(false);
|
||||
});
|
||||
|
||||
it('should not flag URLs as secrets', () => {
|
||||
const text = 'Visit https://example.com/path?query=param&key=value';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(false);
|
||||
});
|
||||
|
||||
it('should not flag short strings', () => {
|
||||
const text = 'Short: abc123';
|
||||
const result = detectHighEntropyStrings(text, { minLength: 24 });
|
||||
|
||||
expect(result.hasSecrets).toBe(false);
|
||||
});
|
||||
|
||||
it('should respect custom entropy threshold', () => {
|
||||
const text = 'Medium entropy: abcdefghijklmnopqrstuvwxyz12345678';
|
||||
const lowThreshold = detectHighEntropyStrings(text, { minEntropyThreshold: 3.0 });
|
||||
const highThreshold = detectHighEntropyStrings(text, { minEntropyThreshold: 6.0 });
|
||||
|
||||
expect(lowThreshold.hasSecrets).toBe(true);
|
||||
expect(highThreshold.hasSecrets).toBe(false);
|
||||
});
|
||||
|
||||
it('should support custom patterns', () => {
|
||||
const text = 'Custom token: CUSTOM-Ab12Cd34Ef56Gh78Ij90Kl12';
|
||||
const result = detectHighEntropyStrings(text, {
|
||||
customPatterns: ['CUSTOM-[A-Za-z0-9]{24}'],
|
||||
});
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
});
|
||||
|
||||
it('should detect multiple secrets in one message', () => {
|
||||
const text =
|
||||
'OpenAI: sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56 and GitHub: ghp_Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op5678Qr';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.hasSecrets).toBe(true);
|
||||
expect(result.secrets.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should sort secrets by position', () => {
|
||||
const text =
|
||||
'First: ghp_Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op5678Qr Second: sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
|
||||
expect(result.secrets[0].start).toBeLessThan(result.secrets[1].start);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redactSecrets', () => {
|
||||
it('should redact detected secrets', () => {
|
||||
const text = 'My API key is sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
const redacted = redactSecrets(text, result.secrets);
|
||||
|
||||
expect(redacted).toBe('My API key is [REDACTED]');
|
||||
});
|
||||
|
||||
it('should use custom placeholder', () => {
|
||||
const text = 'Secret: ghp_Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op5678Qr';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
const redacted = redactSecrets(text, result.secrets, '***');
|
||||
|
||||
expect(redacted).toBe('Secret: ***');
|
||||
});
|
||||
|
||||
it('should redact multiple secrets', () => {
|
||||
const text =
|
||||
'Key1: sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56 Key2: ghp_Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op5678Qr';
|
||||
const result = detectHighEntropyStrings(text);
|
||||
const redacted = redactSecrets(text, result.secrets);
|
||||
|
||||
expect(redacted).toBe('Key1: [REDACTED] Key2: [REDACTED]');
|
||||
});
|
||||
|
||||
it('should handle empty secrets array', () => {
|
||||
const text = 'No secrets here';
|
||||
const redacted = redactSecrets(text, []);
|
||||
|
||||
expect(redacted).toBe(text);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
200
src/security/interactive-flow.test.ts
Normal file
200
src/security/interactive-flow.test.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { processSecretsInMessage } from './process-secrets.js';
|
||||
import {
|
||||
generatePromptKey,
|
||||
resolvePendingPrompt,
|
||||
clearAllPendingPrompts,
|
||||
hasPendingPrompt,
|
||||
} from './interactive-prompts.js';
|
||||
import type { FinalizedMsgContext } from '../auto-reply/templating.js';
|
||||
import type { MoltbotConfig } from '../config/config.js';
|
||||
|
||||
describe('interactive security flow', () => {
|
||||
beforeEach(() => {
|
||||
clearAllPendingPrompts();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearAllPendingPrompts();
|
||||
});
|
||||
|
||||
// Mock dispatcher for testing
|
||||
function createMockDispatcher() {
|
||||
const sentMessages: string[] = [];
|
||||
return {
|
||||
sendText: async (text: string) => {
|
||||
sentMessages.push(text);
|
||||
},
|
||||
messages: sentMessages,
|
||||
};
|
||||
}
|
||||
|
||||
// Mock config with interactive mode enabled
|
||||
const interactiveConfig: MoltbotConfig = {
|
||||
security: {
|
||||
secrets: {
|
||||
detection: {
|
||||
enabled: true,
|
||||
},
|
||||
handling: {
|
||||
interactive: true,
|
||||
defaultAction: 'redact',
|
||||
confirmationTimeoutMs: 5000,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Mock context
|
||||
function createMockContext(body: string): FinalizedMsgContext {
|
||||
return {
|
||||
Body: body,
|
||||
BodyForAgent: body,
|
||||
CommandBody: body,
|
||||
BodyForCommands: body,
|
||||
Provider: 'telegram',
|
||||
SenderId: 'user123',
|
||||
ChatType: 'dm',
|
||||
SessionKey: 'test-session',
|
||||
CommandAuthorized: true,
|
||||
};
|
||||
}
|
||||
|
||||
it('should send prompt and wait for user response (redact action)', async () => {
|
||||
const ctx = createMockContext('My API key is sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56');
|
||||
const dispatcher = createMockDispatcher();
|
||||
|
||||
// Start processing (this will block until user responds)
|
||||
const processPromise = processSecretsInMessage(ctx, interactiveConfig, dispatcher as any);
|
||||
|
||||
// Wait a bit for the prompt to be registered
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Verify prompt was sent
|
||||
expect(dispatcher.messages.length).toBe(1);
|
||||
expect(dispatcher.messages[0]).toContain('🔒 **Security Alert**');
|
||||
expect(dispatcher.messages[0]).toContain('Reply with **1**, **2**, or **3**');
|
||||
|
||||
// Verify pending prompt was registered
|
||||
const promptKey = generatePromptKey('telegram', 'user123');
|
||||
expect(hasPendingPrompt(promptKey)).toBe(true);
|
||||
|
||||
// Simulate user responding with "1" (redact)
|
||||
resolvePendingPrompt(promptKey, 'redact');
|
||||
|
||||
// Wait for processing to complete
|
||||
const result = await processPromise;
|
||||
|
||||
// Verify result
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.modified).toBe(true);
|
||||
expect(result.blocked).toBe(false);
|
||||
expect(result.ctx.Body).toBe('My API key is [REDACTED]');
|
||||
});
|
||||
|
||||
it('should send prompt and wait for user response (cancel action)', async () => {
|
||||
const ctx = createMockContext('Secret: sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56');
|
||||
const dispatcher = createMockDispatcher();
|
||||
|
||||
const processPromise = processSecretsInMessage(ctx, interactiveConfig, dispatcher as any);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const promptKey = generatePromptKey('telegram', 'user123');
|
||||
resolvePendingPrompt(promptKey, 'cancel');
|
||||
|
||||
const result = await processPromise;
|
||||
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.modified).toBe(false);
|
||||
expect(result.blocked).toBe(true); // Message should be blocked
|
||||
});
|
||||
|
||||
it('should send prompt and wait for user response (allow action)', async () => {
|
||||
const ctx = createMockContext('Key: sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56');
|
||||
const dispatcher = createMockDispatcher();
|
||||
|
||||
const processPromise = processSecretsInMessage(ctx, interactiveConfig, dispatcher as any);
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
const promptKey = generatePromptKey('telegram', 'user123');
|
||||
resolvePendingPrompt(promptKey, 'allow');
|
||||
|
||||
const result = await processPromise;
|
||||
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.modified).toBe(false); // Not modified because allowed
|
||||
expect(result.blocked).toBe(false);
|
||||
expect(result.ctx.Body).toContain('sk-proj-'); // Secret still in message
|
||||
});
|
||||
|
||||
it('should timeout and apply default action if no response', async () => {
|
||||
const config: MoltbotConfig = {
|
||||
security: {
|
||||
secrets: {
|
||||
detection: { enabled: true },
|
||||
handling: {
|
||||
interactive: true,
|
||||
defaultAction: 'redact',
|
||||
confirmationTimeoutMs: 100, // Short timeout for testing
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createMockContext('API: sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56');
|
||||
const dispatcher = createMockDispatcher();
|
||||
|
||||
const result = await processSecretsInMessage(ctx, config, dispatcher as any);
|
||||
|
||||
// Should have timed out and applied default action (redact)
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.modified).toBe(true);
|
||||
expect(result.blocked).toBe(false);
|
||||
expect(result.ctx.Body).toBe('API: [REDACTED]');
|
||||
});
|
||||
|
||||
it('should fall back to default action when interactive mode is disabled', async () => {
|
||||
const config: MoltbotConfig = {
|
||||
security: {
|
||||
secrets: {
|
||||
detection: { enabled: true },
|
||||
handling: {
|
||||
interactive: false, // Disabled
|
||||
defaultAction: 'redact',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createMockContext('Key: sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56');
|
||||
const dispatcher = createMockDispatcher();
|
||||
|
||||
const result = await processSecretsInMessage(ctx, config, dispatcher as any);
|
||||
|
||||
// Should have immediately applied default action without prompting
|
||||
expect(result.detected).toBe(true);
|
||||
expect(result.modified).toBe(true);
|
||||
expect(result.blocked).toBe(false);
|
||||
expect(result.ctx.Body).toBe('Key: [REDACTED]');
|
||||
expect(dispatcher.messages.length).toBe(0); // No prompt sent
|
||||
});
|
||||
|
||||
it('should not detect secrets when detection is disabled', async () => {
|
||||
const config: MoltbotConfig = {
|
||||
security: {
|
||||
secrets: {
|
||||
detection: { enabled: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const ctx = createMockContext('Key: sk-proj-Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56Qr78St90Uv12Wx34Yz56');
|
||||
const dispatcher = createMockDispatcher();
|
||||
|
||||
const result = await processSecretsInMessage(ctx, config, dispatcher as any);
|
||||
|
||||
expect(result.detected).toBe(false);
|
||||
expect(result.modified).toBe(false);
|
||||
expect(result.ctx.Body).toContain('sk-proj-'); // Secret still in message
|
||||
});
|
||||
});
|
||||
253
src/security/interactive-prompts.test.ts
Normal file
253
src/security/interactive-prompts.test.ts
Normal file
@ -0,0 +1,253 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import {
|
||||
generatePromptKey,
|
||||
registerPendingPrompt,
|
||||
hasPendingPrompt,
|
||||
getPendingPrompt,
|
||||
resolvePendingPrompt,
|
||||
cancelPendingPrompt,
|
||||
parsePromptResponse,
|
||||
clearAllPendingPrompts,
|
||||
getPendingPromptCount,
|
||||
} from './interactive-prompts.js';
|
||||
import type { DetectedSecret } from './entropy.js';
|
||||
|
||||
describe('interactive-prompts', () => {
|
||||
beforeEach(() => {
|
||||
clearAllPendingPrompts();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearAllPendingPrompts();
|
||||
});
|
||||
|
||||
describe('generatePromptKey', () => {
|
||||
it('should generate a unique key from channel and senderId', () => {
|
||||
const key = generatePromptKey('telegram', 'user123');
|
||||
expect(key).toBe('telegram:user123');
|
||||
});
|
||||
|
||||
it('should generate different keys for different users', () => {
|
||||
const key1 = generatePromptKey('telegram', 'user123');
|
||||
const key2 = generatePromptKey('telegram', 'user456');
|
||||
expect(key1).not.toBe(key2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registerPendingPrompt', () => {
|
||||
it('should register a pending prompt', async () => {
|
||||
const secrets: DetectedSecret[] = [
|
||||
{
|
||||
value: 'sk-abc123',
|
||||
type: 'api_key',
|
||||
start: 0,
|
||||
end: 10,
|
||||
entropy: 4.5,
|
||||
confidence: 'high',
|
||||
},
|
||||
];
|
||||
|
||||
const key = 'telegram:user123';
|
||||
const promise = registerPendingPrompt(key, secrets, 5000);
|
||||
|
||||
expect(hasPendingPrompt(key)).toBe(true);
|
||||
expect(getPendingPromptCount()).toBe(1);
|
||||
|
||||
// Resolve it
|
||||
resolvePendingPrompt(key, 'redact');
|
||||
const result = await promise;
|
||||
expect(result).toBe('redact');
|
||||
expect(hasPendingPrompt(key)).toBe(false);
|
||||
});
|
||||
|
||||
it('should timeout and resolve with null', async () => {
|
||||
const secrets: DetectedSecret[] = [
|
||||
{
|
||||
value: 'sk-abc123',
|
||||
type: 'api_key',
|
||||
start: 0,
|
||||
end: 10,
|
||||
entropy: 4.5,
|
||||
confidence: 'high',
|
||||
},
|
||||
];
|
||||
|
||||
const key = 'telegram:user123';
|
||||
const promise = registerPendingPrompt(key, secrets, 100); // 100ms timeout
|
||||
|
||||
expect(hasPendingPrompt(key)).toBe(true);
|
||||
|
||||
// Wait for timeout
|
||||
const result = await promise;
|
||||
expect(result).toBeNull();
|
||||
expect(hasPendingPrompt(key)).toBe(false);
|
||||
});
|
||||
|
||||
it('should replace existing prompt for same user', async () => {
|
||||
const secrets1: DetectedSecret[] = [
|
||||
{
|
||||
value: 'sk-abc123',
|
||||
type: 'api_key',
|
||||
start: 0,
|
||||
end: 10,
|
||||
entropy: 4.5,
|
||||
confidence: 'high',
|
||||
},
|
||||
];
|
||||
const secrets2: DetectedSecret[] = [
|
||||
{
|
||||
value: 'ghp-xyz789',
|
||||
type: 'api_key',
|
||||
start: 0,
|
||||
end: 10,
|
||||
entropy: 4.5,
|
||||
confidence: 'high',
|
||||
},
|
||||
];
|
||||
|
||||
const key = 'telegram:user123';
|
||||
const promise1 = registerPendingPrompt(key, secrets1, 5000);
|
||||
const promise2 = registerPendingPrompt(key, secrets2, 5000);
|
||||
|
||||
// First promise should resolve with null (replaced)
|
||||
const result1 = await promise1;
|
||||
expect(result1).toBeNull();
|
||||
|
||||
// Second promise should still be pending
|
||||
expect(hasPendingPrompt(key)).toBe(true);
|
||||
|
||||
// Resolve second
|
||||
resolvePendingPrompt(key, 'redact');
|
||||
const result2 = await promise2;
|
||||
expect(result2).toBe('redact');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePendingPrompt', () => {
|
||||
it('should resolve a pending prompt with chosen action', async () => {
|
||||
const secrets: DetectedSecret[] = [
|
||||
{
|
||||
value: 'sk-abc123',
|
||||
type: 'api_key',
|
||||
start: 0,
|
||||
end: 10,
|
||||
entropy: 4.5,
|
||||
confidence: 'high',
|
||||
},
|
||||
];
|
||||
|
||||
const key = 'telegram:user123';
|
||||
const promise = registerPendingPrompt(key, secrets, 5000);
|
||||
|
||||
const resolved = resolvePendingPrompt(key, 'redact');
|
||||
expect(resolved).toBe(true);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe('redact');
|
||||
expect(hasPendingPrompt(key)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if no pending prompt exists', () => {
|
||||
const resolved = resolvePendingPrompt('nonexistent:key', 'store');
|
||||
expect(resolved).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelPendingPrompt', () => {
|
||||
it('should cancel a pending prompt', async () => {
|
||||
const secrets: DetectedSecret[] = [
|
||||
{
|
||||
value: 'sk-abc123',
|
||||
type: 'api_key',
|
||||
start: 0,
|
||||
end: 10,
|
||||
entropy: 4.5,
|
||||
confidence: 'high',
|
||||
},
|
||||
];
|
||||
|
||||
const key = 'telegram:user123';
|
||||
const promise = registerPendingPrompt(key, secrets, 5000);
|
||||
|
||||
const cancelled = cancelPendingPrompt(key);
|
||||
expect(cancelled).toBe(true);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBeNull();
|
||||
expect(hasPendingPrompt(key)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsePromptResponse', () => {
|
||||
it('should parse numeric responses', () => {
|
||||
expect(parsePromptResponse('1')).toBe('redact');
|
||||
expect(parsePromptResponse('2')).toBe('cancel');
|
||||
expect(parsePromptResponse('3')).toBe('allow');
|
||||
});
|
||||
|
||||
it('should parse responses with periods', () => {
|
||||
expect(parsePromptResponse('1.')).toBe('redact');
|
||||
expect(parsePromptResponse('2.')).toBe('cancel');
|
||||
expect(parsePromptResponse('3.')).toBe('allow');
|
||||
});
|
||||
|
||||
it('should parse "option N" responses', () => {
|
||||
expect(parsePromptResponse('option 1')).toBe('redact');
|
||||
expect(parsePromptResponse('option 2')).toBe('cancel');
|
||||
expect(parsePromptResponse('option 3')).toBe('allow');
|
||||
});
|
||||
|
||||
it('should parse keyword responses', () => {
|
||||
expect(parsePromptResponse('redact')).toBe('redact');
|
||||
expect(parsePromptResponse('hide')).toBe('redact');
|
||||
expect(parsePromptResponse('cancel')).toBe('cancel');
|
||||
expect(parsePromptResponse('abort')).toBe('cancel');
|
||||
expect(parsePromptResponse('allow')).toBe('allow');
|
||||
expect(parsePromptResponse('continue')).toBe('allow');
|
||||
});
|
||||
|
||||
it('should be case-insensitive', () => {
|
||||
expect(parsePromptResponse('REDACT')).toBe('redact');
|
||||
expect(parsePromptResponse('Cancel')).toBe('cancel');
|
||||
expect(parsePromptResponse('ALLOW')).toBe('allow');
|
||||
});
|
||||
|
||||
it('should ignore whitespace', () => {
|
||||
expect(parsePromptResponse(' 1 ')).toBe('redact');
|
||||
expect(parsePromptResponse('\n2\n')).toBe('cancel');
|
||||
});
|
||||
|
||||
it('should return null for invalid responses', () => {
|
||||
expect(parsePromptResponse('4')).toBeNull();
|
||||
expect(parsePromptResponse('invalid')).toBeNull();
|
||||
expect(parsePromptResponse('yes')).toBeNull();
|
||||
expect(parsePromptResponse('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAllPendingPrompts', () => {
|
||||
it('should clear all pending prompts', async () => {
|
||||
const secrets: DetectedSecret[] = [
|
||||
{
|
||||
value: 'sk-abc123',
|
||||
type: 'api_key',
|
||||
start: 0,
|
||||
end: 10,
|
||||
entropy: 4.5,
|
||||
confidence: 'high',
|
||||
},
|
||||
];
|
||||
|
||||
const promise1 = registerPendingPrompt('telegram:user1', secrets, 5000);
|
||||
const promise2 = registerPendingPrompt('discord:user2', secrets, 5000);
|
||||
|
||||
expect(getPendingPromptCount()).toBe(2);
|
||||
|
||||
clearAllPendingPrompts();
|
||||
|
||||
expect(getPendingPromptCount()).toBe(0);
|
||||
expect(await promise1).toBeNull();
|
||||
expect(await promise2).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user