Skip to main content
Glama
prefs-reader.test.ts14.7 kB
/** * iOS UserDefaults/Preferences Reader Unit Tests * Tests using dependency-injected shell executor */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { readUserDefaults, readDefaultsDomain, getAppContainerPath, listInstalledApps, isAppInstalled, ReadPreferencesOptions, } from '../../../../src/platforms/ios/prefs-reader.js'; import { ShellExecutor } from '../../../../src/utils/shell-executor.js'; // Create a mock shell executor function createMockShell(): ShellExecutor & { execute: ReturnType<typeof vi.fn>; executeOrThrow: ReturnType<typeof vi.fn>; commandExists: ReturnType<typeof vi.fn>; } { return { execute: vi.fn(), executeOrThrow: vi.fn(), commandExists: vi.fn(), }; } describe('iOS UserDefaults/Preferences Reader', () => { let mockShell: ReturnType<typeof createMockShell>; beforeEach(() => { vi.clearAllMocks(); mockShell = createMockShell(); }); describe('readUserDefaults', () => { const bundleId = 'com.example.app'; it('should read UserDefaults successfully', async () => { // Mock get_app_container mockShell.execute .mockResolvedValueOnce({ stdout: '/Users/test/Library/Developer/CoreSimulator/Devices/123/data/Containers/Data/Application/456', stderr: '', exitCode: 0, }) // Mock listing plist files .mockResolvedValueOnce({ stdout: 'com.example.app.plist\nSettings.plist\n', stderr: '', exitCode: 0, }) // Mock reading first plist file (plutil convert) .mockResolvedValueOnce({ stdout: `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>username</key> <string>john_doe</string> <key>loginCount</key> <integer>5</integer> </dict> </plist>`, stderr: '', exitCode: 0, }) // Mock reading second plist file .mockResolvedValueOnce({ stdout: `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>darkMode</key> <true/> </dict> </plist>`, stderr: '', exitCode: 0, }); const result = await readUserDefaults(bundleId, { shell: mockShell }); expect(result).toHaveLength(2); expect(result[0].name).toBe('com.example.app'); expect(result[1].name).toBe('Settings'); }); it('should filter by fileName when specified', async () => { mockShell.execute .mockResolvedValueOnce({ stdout: '/path/to/container', stderr: '', exitCode: 0, }) .mockResolvedValueOnce({ stdout: 'com.example.app.plist\nSettings.plist\n', stderr: '', exitCode: 0, }) .mockResolvedValueOnce({ stdout: `<?xml version="1.0" encoding="UTF-8"?> <plist version="1.0"><dict><key>key</key><string>value</string></dict></plist>`, stderr: '', exitCode: 0, }); const result = await readUserDefaults(bundleId, { fileName: 'Settings', shell: mockShell, }); expect(result).toHaveLength(1); expect(result[0].name).toBe('Settings'); }); it('should return empty array when container path not found', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '', stderr: 'App not installed', exitCode: 1, }); const result = await readUserDefaults(bundleId, { shell: mockShell }); expect(result).toHaveLength(0); }); it('should try bundle ID plist directly when no files listed', async () => { mockShell.execute // Container path .mockResolvedValueOnce({ stdout: '/path/to/container', stderr: '', exitCode: 0, }) // List returns empty .mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0, }) // Try reading bundle ID plist directly .mockResolvedValueOnce({ stdout: `<?xml version="1.0" encoding="UTF-8"?> <plist version="1.0"><dict><key>key</key><string>value</string></dict></plist>`, stderr: '', exitCode: 0, }); const result = await readUserDefaults(bundleId, { shell: mockShell }); expect(result).toHaveLength(1); expect(result[0].name).toBe(bundleId); }); it('should use custom deviceId when provided', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 1, }); await readUserDefaults(bundleId, { deviceId: 'ABCD-1234-5678', shell: mockShell, }); expect(mockShell.execute).toHaveBeenCalledWith( 'xcrun', expect.arrayContaining(['ABCD-1234-5678']), expect.any(Object) ); }); it('should handle timeout option', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 1, }); await readUserDefaults(bundleId, { timeoutMs: 5000, shell: mockShell, }); expect(mockShell.execute).toHaveBeenCalledWith( 'xcrun', expect.any(Array), expect.objectContaining({ timeoutMs: 5000 }) ); }); it('should fall back to cat when plutil fails', async () => { mockShell.execute .mockResolvedValueOnce({ stdout: '/path/to/container', stderr: '', exitCode: 0, }) .mockResolvedValueOnce({ stdout: 'test.plist\n', stderr: '', exitCode: 0, }) // plutil fails .mockResolvedValueOnce({ stdout: '', stderr: 'plutil failed', exitCode: 1, }) // cat succeeds .mockResolvedValueOnce({ stdout: `<?xml version="1.0" encoding="UTF-8"?> <plist version="1.0"><dict><key>key</key><string>value</string></dict></plist>`, stderr: '', exitCode: 0, }); const result = await readUserDefaults(bundleId, { shell: mockShell }); expect(result).toHaveLength(1); // Verify cat was called expect(mockShell.execute).toHaveBeenCalledWith( 'xcrun', expect.arrayContaining(['cat']), expect.any(Object) ); }); it('should skip files that fail to read', async () => { mockShell.execute .mockResolvedValueOnce({ stdout: '/path/to/container', stderr: '', exitCode: 0, }) .mockResolvedValueOnce({ stdout: 'good.plist\nbad.plist\n', stderr: '', exitCode: 0, }) // First file succeeds .mockResolvedValueOnce({ stdout: `<?xml version="1.0" encoding="UTF-8"?> <plist version="1.0"><dict><key>key</key><string>value</string></dict></plist>`, stderr: '', exitCode: 0, }) // Second file fails (plutil) .mockResolvedValueOnce({ stdout: '', stderr: 'Error', exitCode: 1, }) // Second file fails (cat fallback) .mockResolvedValueOnce({ stdout: '', stderr: 'Error', exitCode: 1, }); const result = await readUserDefaults(bundleId, { shell: mockShell }); expect(result).toHaveLength(1); expect(result[0].name).toBe('good'); }); }); describe('getAppContainerPath', () => { const bundleId = 'com.example.app'; it('should return container path when app is found', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '/path/to/container\n', stderr: '', exitCode: 0, }); const result = await getAppContainerPath(bundleId, 'booted', 5000, mockShell); expect(result).toBe('/path/to/container'); expect(mockShell.execute).toHaveBeenCalledWith( 'xcrun', ['simctl', 'get_app_container', 'booted', bundleId, 'data'], expect.any(Object) ); }); it('should return null when app is not found', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '', stderr: 'App not installed', exitCode: 1, }); const result = await getAppContainerPath(bundleId, 'booted', 5000, mockShell); expect(result).toBeNull(); }); it('should use custom deviceId', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '/path/to/container', stderr: '', exitCode: 0, }); await getAppContainerPath(bundleId, 'DEVICE-123', 5000, mockShell); expect(mockShell.execute).toHaveBeenCalledWith( 'xcrun', expect.arrayContaining(['DEVICE-123']), expect.any(Object) ); }); it('should return null when execution throws', async () => { mockShell.execute.mockRejectedValueOnce(new Error('Command failed')); const result = await getAppContainerPath(bundleId, 'booted', 5000, mockShell); expect(result).toBeNull(); }); }); describe('readDefaultsDomain', () => { const bundleId = 'com.example.app'; it('should read defaults domain successfully', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: `{ username = "john_doe"; loginCount = 5; isEnabled = 1; temperature = "23.5"; }`, stderr: '', exitCode: 0, }); const result = await readDefaultsDomain(bundleId, 'booted', 10000, mockShell); expect(result).not.toBeNull(); expect(result!.name).toBe(bundleId); expect(result!.entries.length).toBeGreaterThan(0); }); it('should return null when domain not found', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '', stderr: 'Domain does not exist', exitCode: 1, }); const result = await readDefaultsDomain(bundleId, 'booted', 10000, mockShell); expect(result).toBeNull(); }); it('should use custom deviceId', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '{}', stderr: '', exitCode: 0, }); await readDefaultsDomain(bundleId, 'DEVICE-456', 10000, mockShell); expect(mockShell.execute).toHaveBeenCalledWith( 'xcrun', expect.arrayContaining(['DEVICE-456']), expect.any(Object) ); }); it('should return null when execution throws', async () => { mockShell.execute.mockRejectedValueOnce(new Error('Command failed')); const result = await readDefaultsDomain(bundleId, 'booted', 10000, mockShell); expect(result).toBeNull(); }); it('should parse boolean values based on key names', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: `{ isEnabled = 1; enableNotifications = 0; count = 1; }`, stderr: '', exitCode: 0, }); const result = await readDefaultsDomain(bundleId, 'booted', 10000, mockShell); expect(result).not.toBeNull(); // isEnabled should be boolean true const isEnabled = result!.entries.find((e) => e.key === 'isEnabled'); expect(isEnabled?.type).toBe('boolean'); expect(isEnabled?.value).toBe(true); // enableNotifications should be boolean false const enableNotifications = result!.entries.find((e) => e.key === 'enableNotifications'); expect(enableNotifications?.type).toBe('boolean'); expect(enableNotifications?.value).toBe(false); // count should be int (no enable/is pattern) const count = result!.entries.find((e) => e.key === 'count'); expect(count?.type).toBe('int'); expect(count?.value).toBe(1); }); }); describe('listInstalledApps', () => { it('should list installed apps', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: `{ CFBundleIdentifier = "com.example.app1"; CFBundleName = "App 1"; } { CFBundleIdentifier = "com.example.app2"; CFBundleName = "App 2"; }`, stderr: '', exitCode: 0, }); const result = await listInstalledApps('booted', 10000, mockShell); expect(result).toHaveLength(2); expect(result).toContain('com.example.app1'); expect(result).toContain('com.example.app2'); }); it('should return empty array when command fails', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '', stderr: 'Error', exitCode: 1, }); const result = await listInstalledApps('booted', 10000, mockShell); expect(result).toHaveLength(0); }); it('should use custom deviceId', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0, }); await listInstalledApps('DEVICE-789', 10000, mockShell); expect(mockShell.execute).toHaveBeenCalledWith( 'xcrun', ['simctl', 'listapps', 'DEVICE-789'], expect.any(Object) ); }); it('should return empty array when execution throws', async () => { mockShell.execute.mockRejectedValueOnce(new Error('Command failed')); const result = await listInstalledApps('booted', 10000, mockShell); expect(result).toHaveLength(0); }); }); describe('isAppInstalled', () => { const bundleId = 'com.example.app'; it('should return true when app is installed', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '/path/to/container', stderr: '', exitCode: 0, }); const result = await isAppInstalled(bundleId, 'booted', mockShell); expect(result).toBe(true); }); it('should return false when app is not installed', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '', stderr: 'App not found', exitCode: 1, }); const result = await isAppInstalled(bundleId, 'booted', mockShell); expect(result).toBe(false); }); it('should use custom deviceId', async () => { mockShell.execute.mockResolvedValueOnce({ stdout: '/path/to/container', stderr: '', exitCode: 0, }); await isAppInstalled(bundleId, 'DEVICE-ABC', mockShell); expect(mockShell.execute).toHaveBeenCalledWith( 'xcrun', expect.arrayContaining(['DEVICE-ABC']), expect.any(Object) ); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abd3lraouf/specter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server