import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
getPermissionInstructions,
checkScreenRecordingPermission,
checkAccessibilityPermission,
checkAllPermissions,
ensurePermissions,
openPermissionSettings,
setExecAsync,
resetExecAsync,
PermissionStatus
} from '../../src/permissions.js';
import { PermissionError } from '../../src/errors.js';
// Create a mock execAsync function
const mockExecAsync = vi.fn();
describe('permissions - comprehensive tests', () => {
beforeEach(() => {
vi.clearAllMocks();
// Inject our mock function (these functions are only available in test environment)
if (setExecAsync) {
setExecAsync(mockExecAsync);
}
// Default mock response
mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
});
afterEach(() => {
// Reset to real implementation (only available in test environment)
if (resetExecAsync) {
resetExecAsync();
}
});
describe('getPermissionInstructions', () => {
it('should return helpful permission instructions', () => {
const instructions = getPermissionInstructions();
expect(instructions).toContain('Screen Recording Permission');
expect(instructions).toContain('Accessibility Permission');
expect(instructions).toContain('System Preferences');
expect(instructions).toContain('Security & Privacy');
expect(instructions).toContain('Privacy');
expect(instructions).not.toContain('undefined');
expect(instructions.trim()).not.toBe('');
});
it('should include both permission types in instructions', () => {
const instructions = getPermissionInstructions();
expect(instructions).toMatch(/Screen Recording.*Accessibility/s);
});
it('should include restart instructions', () => {
const instructions = getPermissionInstructions();
expect(instructions).toContain('restart');
expect(instructions).toContain('application');
});
it('should include unlock instruction', () => {
const instructions = getPermissionInstructions();
expect(instructions).toContain('unlock');
expect(instructions).toContain('lock icon');
});
it('should be properly formatted', () => {
const instructions = getPermissionInstructions();
// Should start and end without extra whitespace
expect(instructions).toBe(instructions.trim());
// Should contain numbered steps
expect(instructions).toMatch(/1\./);
expect(instructions).toMatch(/2\./);
// Should contain proper sections
expect(instructions).toContain('Mac Commander MCP');
});
it('should provide step-by-step instructions', () => {
const instructions = getPermissionInstructions();
// Check for specific instruction details
expect(instructions).toContain('terminal application or Node.js');
expect(instructions).toContain('Check the box');
expect(instructions).toContain('left sidebar');
});
});
describe('checkScreenRecordingPermission', () => {
it('should return true when permission is granted', async () => {
mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
const result = await checkScreenRecordingPermission();
expect(result).toBe(true);
expect(mockExecAsync).toHaveBeenCalledWith('screencapture -x -t png /dev/null 2>&1');
});
it('should return false when permission is denied', async () => {
mockExecAsync.mockResolvedValue({
stdout: '',
stderr: 'screencapture: cannot run without screen recording permission'
});
const result = await checkScreenRecordingPermission();
expect(result).toBe(false);
});
it('should return false when command fails', async () => {
mockExecAsync.mockRejectedValue(new Error('Command failed'));
const result = await checkScreenRecordingPermission();
expect(result).toBe(false);
});
it('should handle stderr with additional error messages', async () => {
mockExecAsync.mockResolvedValue({
stdout: '',
stderr: 'Error: screencapture: cannot run without screen recording permission. Additional info.'
});
const result = await checkScreenRecordingPermission();
expect(result).toBe(false);
});
});
describe('checkAccessibilityPermission', () => {
it('should return true when permission is granted', async () => {
mockExecAsync.mockResolvedValue({ stdout: 'true\n', stderr: '' });
const result = await checkAccessibilityPermission();
expect(result).toBe(true);
expect(mockExecAsync).toHaveBeenCalledWith(expect.stringContaining('osascript'));
expect(mockExecAsync).toHaveBeenCalledWith(expect.stringContaining('UI elements enabled'));
});
it('should return false when permission is denied', async () => {
mockExecAsync.mockResolvedValue({ stdout: 'false\n', stderr: '' });
const result = await checkAccessibilityPermission();
expect(result).toBe(false);
});
it('should return false when AppleScript fails', async () => {
mockExecAsync.mockRejectedValue(new Error('AppleScript error'));
const result = await checkAccessibilityPermission();
expect(result).toBe(false);
});
it('should handle unexpected output', async () => {
mockExecAsync.mockResolvedValue({ stdout: 'unexpected output', stderr: '' });
const result = await checkAccessibilityPermission();
expect(result).toBe(false);
});
it('should handle whitespace in output', async () => {
mockExecAsync.mockResolvedValue({ stdout: ' true \n\n', stderr: '' });
const result = await checkAccessibilityPermission();
expect(result).toBe(true);
});
});
describe('checkAllPermissions', () => {
it('should check both permissions and return status', async () => {
// Mock both permissions as granted
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: '' }) // screen recording
.mockResolvedValueOnce({ stdout: 'true', stderr: '' }); // accessibility
const result = await checkAllPermissions();
expect(result).toEqual({
screenRecording: true,
accessibility: true,
errors: []
});
});
it('should include error messages when permissions are denied', async () => {
// Mock both permissions as denied
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: 'screencapture: cannot run without screen recording permission' })
.mockResolvedValueOnce({ stdout: 'false', stderr: '' });
const result = await checkAllPermissions();
expect(result.screenRecording).toBe(false);
expect(result.accessibility).toBe(false);
expect(result.errors).toHaveLength(2);
expect(result.errors[0]).toContain('Screen Recording permission is required');
expect(result.errors[1]).toContain('Accessibility permission is required');
});
it('should handle partial permissions', async () => {
// Mock screen recording granted, accessibility denied
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: '' })
.mockResolvedValueOnce({ stdout: 'false', stderr: '' });
const result = await checkAllPermissions();
expect(result.screenRecording).toBe(true);
expect(result.accessibility).toBe(false);
expect(result.errors).toHaveLength(1);
expect(result.errors[0]).toContain('Accessibility permission is required');
});
it('should check permissions concurrently', async () => {
let callCount = 0;
mockExecAsync.mockImplementation(() => {
callCount++;
return Promise.resolve({ stdout: 'true', stderr: '' });
});
const startTime = Date.now();
await checkAllPermissions();
const endTime = Date.now();
// Both calls should happen concurrently
expect(callCount).toBe(2);
// Should complete quickly (not sequentially)
expect(endTime - startTime).toBeLessThan(100);
});
});
describe('ensurePermissions', () => {
it('should not throw when all required permissions are granted', async () => {
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: '' })
.mockResolvedValueOnce({ stdout: 'true', stderr: '' });
await expect(ensurePermissions({
screenRecording: true,
accessibility: true
})).resolves.toBeUndefined();
});
it('should throw PermissionError when screen recording is required but not granted', async () => {
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: 'screencapture: cannot run without screen recording permission' })
.mockResolvedValueOnce({ stdout: 'true', stderr: '' });
await expect(ensurePermissions({ screenRecording: true }))
.rejects.toThrow(PermissionError);
try {
await ensurePermissions({ screenRecording: true });
} catch (error) {
expect(error).toBeInstanceOf(PermissionError);
expect((error as PermissionError).permission).toBe('screenRecording');
expect((error as PermissionError).message).toContain('Screen Recording permission is required');
}
});
it('should throw PermissionError when accessibility is required but not granted', async () => {
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: '' })
.mockResolvedValueOnce({ stdout: 'false', stderr: '' });
await expect(ensurePermissions({ accessibility: true }))
.rejects.toThrow(PermissionError);
try {
await ensurePermissions({ accessibility: true });
} catch (error) {
expect(error).toBeInstanceOf(PermissionError);
expect((error as PermissionError).permission).toBe('accessibility');
expect((error as PermissionError).message).toContain('Accessibility permission is required');
}
});
it('should not check permissions that are not required', async () => {
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: 'screencapture: cannot run without screen recording permission' })
.mockResolvedValueOnce({ stdout: 'false', stderr: '' });
// Should not throw even though permissions are denied, because they're not required
await expect(ensurePermissions({})).resolves.toBeUndefined();
});
it('should include status details in error', async () => {
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: 'screencapture: cannot run without screen recording permission' })
.mockResolvedValueOnce({ stdout: 'false', stderr: '' });
try {
await ensurePermissions({ screenRecording: true });
} catch (error) {
expect((error as PermissionError).details).toHaveProperty('status');
expect((error as PermissionError).details.status).toMatchObject({
screenRecording: false,
accessibility: false,
errors: expect.any(Array)
});
}
});
});
describe('openPermissionSettings', () => {
it('should open screen recording settings', async () => {
mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
await openPermissionSettings('screenRecording');
expect(mockExecAsync).toHaveBeenCalledWith(
'open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"'
);
});
it('should open accessibility settings', async () => {
mockExecAsync.mockResolvedValue({ stdout: '', stderr: '' });
await openPermissionSettings('accessibility');
expect(mockExecAsync).toHaveBeenCalledWith(
'open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"'
);
});
it('should throw error when open command fails', async () => {
mockExecAsync.mockRejectedValue(new Error('Open command failed'));
await expect(openPermissionSettings('screenRecording'))
.rejects.toThrow('Failed to open System Preferences: Error: Open command failed');
});
it('should handle invalid permission types', async () => {
// TypeScript would prevent this, but testing runtime behavior
await expect(openPermissionSettings('invalid' as any))
.rejects.toThrow();
});
});
describe('Edge Cases and Error Handling', () => {
it('should handle exec returning null/undefined', async () => {
mockExecAsync.mockResolvedValue(null);
const result = await checkScreenRecordingPermission();
expect(result).toBe(false); // null/undefined result should be treated as failure
});
it('should handle very long error messages', async () => {
const longError = 'screencapture: cannot run without screen recording permission' + 'x'.repeat(1000);
mockExecAsync.mockResolvedValue({ stdout: '', stderr: longError });
const result = await checkScreenRecordingPermission();
expect(result).toBe(false);
});
it('should handle concurrent permission checks', async () => {
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: '' })
.mockResolvedValueOnce({ stdout: 'true', stderr: '' });
const results = await Promise.all([
checkScreenRecordingPermission(),
checkAccessibilityPermission(),
checkScreenRecordingPermission()
]);
expect(results).toEqual([true, true, true]);
});
it('should handle system locale differences', async () => {
// Different system locales might return different error messages
mockExecAsync.mockResolvedValue({
stdout: '',
stderr: 'screencapture: 画面収録の権限なしでは実行できません' // Japanese error message
});
const result = await checkScreenRecordingPermission();
// Should detect permission granted since it doesn't match the English error pattern
expect(result).toBe(true); // Different error message, so it doesn't match the English check
});
});
describe('Integration Scenarios', () => {
it('should handle rapid permission checks', async () => {
mockExecAsync.mockResolvedValue({ stdout: 'true', stderr: '' });
// Simulate rapid permission checks
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(checkAllPermissions());
}
const results = await Promise.all(promises);
expect(results).toHaveLength(10);
expect(results.every(r => r.screenRecording && r.accessibility)).toBe(true);
});
it('should provide clear error path for users', async () => {
// Simulate no permissions scenario
mockExecAsync
.mockResolvedValueOnce({ stdout: '', stderr: 'screencapture: cannot run without screen recording permission' })
.mockResolvedValueOnce({ stdout: 'false', stderr: '' });
try {
await ensurePermissions({ screenRecording: true, accessibility: true });
} catch (error) {
// User should get clear error message
expect(error).toBeInstanceOf(PermissionError);
// They can then get instructions
const instructions = getPermissionInstructions();
expect(instructions).toBeTruthy();
// And open settings (would actually open if not mocked)
await expect(openPermissionSettings('screenRecording')).resolves.toBeUndefined();
}
});
});
});