import { beforeEach, describe, expect, spyOn, it } from 'bun:test' import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { homedir, tmpdir } from 'node:os' import { join } from 'node:path' import { TeamsTokenExtractor } from './token-extractor' describe('TeamsTokenExtractor', () => { let extractor: TeamsTokenExtractor beforeEach(() => { extractor = new TeamsTokenExtractor() }) describe('getDesktopCookiesPaths', () => { it('returns darwin desktop paths on macOS', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const paths = darwinExtractor.getDesktopCookiesPaths() const darwinEbWebView = join( homedir(), 'Library', 'Containers', 'com.microsoft.teams2', 'Data', 'Library', 'Application Support', 'Microsoft', 'MSTeams', 'EBWebView', ) expect(paths).toEqual([ { path: join(darwinEbWebView, 'WV2Profile_tfw', 'Cookies'), accountType: 'work', accountTypeKnown: true }, { path: join(darwinEbWebView, 'WV2Profile_tfw', 'Network', 'Cookies'), accountType: 'work', accountTypeKnown: true, }, { path: join(darwinEbWebView, 'WV2Profile_tfl', 'Cookies'), accountType: 'personal', accountTypeKnown: true, }, { path: join(darwinEbWebView, 'WV2Profile_tfl', 'Network', 'Cookies'), accountType: 'personal', accountTypeKnown: true, }, { path: join(darwinEbWebView, 'Default', 'Cookies'), accountType: 'work', accountTypeKnown: false }, { path: join(darwinEbWebView, 'Default', 'Network', 'Cookies'), accountType: 'work', accountTypeKnown: false }, { path: join(homedir(), 'Library', 'Application Support', 'Microsoft', 'Teams', 'Cookies'), accountType: 'work', accountTypeKnown: false, }, ]) }) it('returns linux desktop path on Linux', () => { const linuxExtractor = new TeamsTokenExtractor('linux') const paths = linuxExtractor.getDesktopCookiesPaths() expect(paths).toEqual([ { path: join(homedir(), '.config', 'Microsoft', 'Microsoft Teams', 'Cookies'), accountType: 'work', accountTypeKnown: false, }, ]) }) it('returns win32 desktop paths on Windows', () => { const winExtractor = new TeamsTokenExtractor('win32') const paths = winExtractor.getDesktopCookiesPaths() const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local') const appdata = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming') const winEbWebView = join( localAppData, 'Packages', 'MSTeams_8wekyb3d8bbwe', 'LocalCache', 'Microsoft', 'MSTeams', 'EBWebView', ) expect(paths).toEqual([ { path: join(winEbWebView, 'WV2Profile_tfw', 'Cookies'), accountType: 'work', accountTypeKnown: true }, { path: join(winEbWebView, 'WV2Profile_tfw', 'Network', 'Cookies'), accountType: 'work', accountTypeKnown: true, }, { path: join(winEbWebView, 'WV2Profile_tfl', 'Cookies'), accountType: 'personal', accountTypeKnown: true }, { path: join(winEbWebView, 'WV2Profile_tfl', 'Network', 'Cookies'), accountType: 'personal', accountTypeKnown: true, }, { path: join(winEbWebView, 'Default', 'Cookies'), accountType: 'work', accountTypeKnown: false }, { path: join(winEbWebView, 'Default', 'Network', 'Cookies'), accountType: 'work', accountTypeKnown: false }, { path: join(appdata, 'Microsoft', 'Teams', 'Cookies'), accountType: 'work', accountTypeKnown: false }, ]) }) it('returns empty array for unsupported platform', () => { const unsupportedExtractor = new TeamsTokenExtractor('freebsd' as NodeJS.Platform) expect(unsupportedExtractor.getDesktopCookiesPaths()).toEqual([]) }) }) describe('getBrowserCookiesPaths', () => { it('returns browser cookie paths on macOS (at least Default profile per browser)', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const paths = darwinExtractor.getBrowserCookiesPaths() const chromeBase = join(homedir(), 'Library', 'Application Support', 'Google', 'Chrome') expect(paths).toContainEqual({ path: join(chromeBase, 'Default', 'Cookies'), accountType: 'work', accountTypeKnown: false, }) expect(paths).toContainEqual({ path: join(chromeBase, 'Default', 'Network', 'Cookies'), accountType: 'work', accountTypeKnown: false, }) }) it('returns browser cookie paths on Linux', () => { const linuxExtractor = new TeamsTokenExtractor('linux') const paths = linuxExtractor.getBrowserCookiesPaths() const chromeBase = join(homedir(), '.config', 'google-chrome') expect(paths).toContainEqual({ path: join(chromeBase, 'Default', 'Cookies'), accountType: 'work', accountTypeKnown: false, }) }) it('returns browser cookie paths on Windows', () => { const winExtractor = new TeamsTokenExtractor('win32') const paths = winExtractor.getBrowserCookiesPaths() const localAppData = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local') const chromeBase = join(localAppData, 'Google', 'Chrome', 'User Data') expect(paths).toContainEqual({ path: join(chromeBase, 'Default', 'Cookies'), accountType: 'work', accountTypeKnown: false, }) }) it('returns empty array for unsupported platform', () => { const unsupportedExtractor = new TeamsTokenExtractor('freebsd' as NodeJS.Platform) expect(unsupportedExtractor.getBrowserCookiesPaths()).toEqual([]) }) // Regression for #163: browser paths must not assert accountType confidently because // Chromium profile paths don't encode work vs personal. Desktop WV2Profile_tfw/_tfl // paths are authoritative; browsers must be probed at validation time. it('browser paths have accountTypeKnown=false so they get probed at validation', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const paths = darwinExtractor.getBrowserCookiesPaths() expect(paths.length).toBeGreaterThan(0) expect(paths.every((p) => p.accountTypeKnown === false)).toBe(true) }) }) describe('getTeamsCookiesPaths', () => { it('returns darwin paths on macOS with desktop paths first', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const paths = darwinExtractor.getTeamsCookiesPaths() const desktopPaths = darwinExtractor.getDesktopCookiesPaths() expect(paths.slice(0, desktopPaths.length)).toEqual(desktopPaths) }) it('browser paths come after desktop paths on macOS', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const paths = darwinExtractor.getTeamsCookiesPaths() const desktopPaths = darwinExtractor.getDesktopCookiesPaths() const browserPaths = darwinExtractor.getBrowserCookiesPaths() expect(paths.length).toBe(desktopPaths.length + browserPaths.length) expect(paths.slice(desktopPaths.length)).toEqual(browserPaths) }) it('returns linux paths with desktop first then browser paths', () => { const linuxExtractor = new TeamsTokenExtractor('linux') const paths = linuxExtractor.getTeamsCookiesPaths() const desktopPaths = linuxExtractor.getDesktopCookiesPaths() expect(paths.slice(0, 1)).toEqual([ { path: join(homedir(), '.config', 'Microsoft', 'Microsoft Teams', 'Cookies'), accountType: 'work', accountTypeKnown: false, }, ]) expect(paths.length).toBeGreaterThan(desktopPaths.length) }) it('returns win32 paths with desktop first then browser paths', () => { const winExtractor = new TeamsTokenExtractor('win32') const paths = winExtractor.getTeamsCookiesPaths() const desktopPaths = winExtractor.getDesktopCookiesPaths() expect(paths.slice(0, desktopPaths.length)).toEqual(desktopPaths) expect(paths.length).toBeGreaterThan(desktopPaths.length) }) it('returns empty array for unsupported platform', () => { const unsupportedExtractor = new TeamsTokenExtractor('freebsd' as NodeJS.Platform) const paths = unsupportedExtractor.getTeamsCookiesPaths() expect(paths).toEqual([]) }) }) describe('getLocalStatePath', () => { it('returns darwin Local State path on macOS', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const path = darwinExtractor.getLocalStatePath() expect(path).toBe(join(homedir(), 'Library', 'Application Support', 'Microsoft', 'Teams', 'Local State')) }) it('returns linux Local State path on Linux', () => { const linuxExtractor = new TeamsTokenExtractor('linux') const path = linuxExtractor.getLocalStatePath() expect(path).toBe(join(homedir(), '.config', 'Microsoft', 'Microsoft Teams', 'Local State')) }) it('returns win32 Local State path on Windows', () => { const winExtractor = new TeamsTokenExtractor('win32') const path = winExtractor.getLocalStatePath() const appdata = process.env.APPDATA || join(homedir(), 'AppData', 'Roaming') expect(path).toBe(join(appdata, 'Microsoft', 'Teams', 'Local State')) }) }) describe('getKeychainVariants', () => { it('includes Teams-specific keychain entries', () => { const macExtractor = new TeamsTokenExtractor('darwin') const variants = macExtractor.getKeychainVariants() expect(variants).toContainEqual({ service: 'Microsoft Teams Safe Storage', account: 'Microsoft Teams' }) expect(variants).toContainEqual({ service: 'Microsoft Teams (work or school) Safe Storage', account: 'Microsoft Teams (work or school)', }) expect(variants).toContainEqual({ service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' }) expect(variants).toContainEqual({ service: 'Teams Safe Storage', account: 'Teams' }) }) it('includes browser keychain entries appended after Teams entries', () => { const macExtractor = new TeamsTokenExtractor('darwin') const variants = macExtractor.getKeychainVariants() expect(variants).toContainEqual({ service: 'Chrome Safe Storage', account: 'Chrome' }) expect(variants).toContainEqual({ service: 'Chrome Canary Safe Storage', account: 'Chrome Canary' }) expect(variants).toContainEqual({ service: 'Arc Safe Storage', account: 'Arc' }) expect(variants).toContainEqual({ service: 'Brave Safe Storage', account: 'Brave' }) expect(variants).toContainEqual({ service: 'Vivaldi Safe Storage', account: 'Vivaldi' }) expect(variants).toContainEqual({ service: 'Chromium Safe Storage', account: 'Chromium' }) }) it('Teams entries come before browser entries', () => { const macExtractor = new TeamsTokenExtractor('darwin') const variants = macExtractor.getKeychainVariants() const teamsIdx = variants.findIndex((v) => v.service === 'Microsoft Teams Safe Storage') const chromeIdx = variants.findIndex((v) => v.service === 'Chrome Safe Storage') expect(teamsIdx).toBeLessThan(chromeIdx) }) }) describe('isValidSkypeToken', () => { it('validates JWT-like skype token format', () => { const validToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature' expect(extractor.isValidSkypeToken(validToken)).toBe(true) }) it('validates long base64 token format', () => { const validToken = 'a'.repeat(100) expect(extractor.isValidSkypeToken(validToken)).toBe(true) }) it('rejects empty tokens', () => { expect(extractor.isValidSkypeToken('')).toBe(false) }) it('rejects short tokens', () => { expect(extractor.isValidSkypeToken('short')).toBe(false) }) it('rejects null/undefined', () => { expect(extractor.isValidSkypeToken(null as unknown as string)).toBe(false) expect(extractor.isValidSkypeToken(undefined as unknown as string)).toBe(false) }) // Regression for #156: PowerShell CLIXML leaks into DPAPI output on Windows. it('rejects CLIXML progress-stream contamination', () => { // given: the exact shape of the leak reported in #156 const clixml = '#< CLIXML\r\n' + 'System.Management.Automation.PSCustomObject' + 'System.Object1' // then expect(extractor.isValidSkypeToken(clixml)).toBe(false) }) it('rejects anything containing angle brackets or whitespace', () => { expect(extractor.isValidSkypeToken('a'.repeat(60) + '
')).toBe(false) expect(extractor.isValidSkypeToken('a'.repeat(40) + ' ' + 'b'.repeat(40))).toBe(false) expect(extractor.isValidSkypeToken('a'.repeat(40) + '\n' + 'b'.repeat(40))).toBe(false) }) it('rejects a bare 36-char UUID', () => { expect(extractor.isValidSkypeToken('12345678-1234-1234-1234-123456789012')).toBe(false) }) }) describe('isEncryptedValue', () => { it('detects v10 encrypted values', () => { const encrypted = Buffer.from('v10encrypted_data') expect(extractor.isEncryptedValue(encrypted)).toBe(true) }) it('detects v11 encrypted values', () => { const encrypted = Buffer.from('v11encrypted_data') expect(extractor.isEncryptedValue(encrypted)).toBe(true) }) it('rejects non-encrypted values', () => { const plain = Buffer.from('plain_text') expect(extractor.isEncryptedValue(plain)).toBe(false) }) it('rejects empty buffers', () => { const empty = Buffer.alloc(0) expect(extractor.isEncryptedValue(empty)).toBe(false) }) it('rejects short buffers', () => { const short = Buffer.from('v1') expect(extractor.isEncryptedValue(short)).toBe(false) }) }) describe('extract', () => { it('returns null when cookies path does not exist', async () => { const linuxExtractor = new TeamsTokenExtractor('linux') const extractFromCookiesDBSpy = spyOn(linuxExtractor as any, 'extractFromCookiesDB').mockResolvedValue([]) const result = await linuxExtractor.extract() expect(result).toEqual([]) extractFromCookiesDBSpy.mockRestore() }) it('extracts token from cookies database when available', async () => { const mockToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature_here' const linuxExtractor = new TeamsTokenExtractor('linux') const extractFromCookiesDBSpy = spyOn(linuxExtractor as any, 'extractFromCookiesDB').mockResolvedValue([ { token: mockToken, accountType: 'work', accountTypeKnown: true }, ]) const result = await linuxExtractor.extract() expect(result).toHaveLength(1) expect(result[0].token).toBe(mockToken) extractFromCookiesDBSpy.mockRestore() }) it('returns null when extraction fails', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const extractFromCookiesDBSpy = spyOn(darwinExtractor as any, 'extractFromCookiesDB').mockResolvedValue([]) const result = await darwinExtractor.extract() expect(result).toEqual([]) extractFromCookiesDBSpy.mockRestore() }) }) describe('extractFromCookiesDB (Network/Cookies fallback)', () => { const mockToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature_here' let workDir: string beforeEach(() => { workDir = mkdtempSync(join(tmpdir(), 'teams-extractor-test-')) }) const cleanup = () => rmSync(workDir, { recursive: true, force: true }) // Regression for #156: if only Network/Cookies exists, missing sibling must not poison accountType. it('falls through to Network/Cookies when Cookies is missing', async () => { // given: only Network/Cookies exists on disk for WV2Profile_tfl const profileDir = join(workDir, 'WV2Profile_tfl') const networkDir = join(profileDir, 'Network') mkdirSync(networkDir, { recursive: true }) const cookiesPath = join(profileDir, 'Cookies') const networkCookiesPath = join(networkDir, 'Cookies') writeFileSync(networkCookiesPath, '') const winExtractor = new TeamsTokenExtractor('win32') const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([ { path: cookiesPath, accountType: 'personal', accountTypeKnown: true }, { path: networkCookiesPath, accountType: 'personal', accountTypeKnown: true }, ]) const tried: string[] = [] const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract').mockImplementation(async (...args) => { const path = args[0] as string tried.push(path) return mockToken }) // when const results = await (winExtractor as any).extractFromCookiesDB() // then: the Cookies path was skipped (never passed to copyAndExtract), // the Network/Cookies sibling was tried, and the token was returned. expect(tried).toEqual([networkCookiesPath]) expect(results).toEqual([{ token: mockToken, accountType: 'personal', accountTypeKnown: true }]) getPathsSpy.mockRestore() copyAndExtractSpy.mockRestore() cleanup() }) // Regression for #156: CLIXML-contaminated decrypt output must not short-circuit // the work account, leaving other valid paths unvisited. it('does not mark accountType seen when the first path yields CLIXML garbage', async () => { // given: first work path returns CLIXML garbage, second work path returns a real token const clixmlGarbage = '#< CLIXML\r\n' + 'Progress\r\n' + 'a'.repeat(80) const realToken = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.signature_here' const winExtractor = new TeamsTokenExtractor('win32') const firstPath = join(workDir, 'WV2Profile_tfw', 'Cookies') const secondPath = join(workDir, 'Default', 'Network', 'Cookies') const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([ { path: firstPath, accountType: 'work', accountTypeKnown: true }, { path: secondPath, accountType: 'work', accountTypeKnown: true }, ]) mkdirSync(join(workDir, 'WV2Profile_tfw'), { recursive: true }) mkdirSync(join(workDir, 'Default', 'Network'), { recursive: true }) writeFileSync(firstPath, '') writeFileSync(secondPath, '') const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract') .mockResolvedValueOnce(clixmlGarbage) .mockResolvedValueOnce(realToken) // when const results = await (winExtractor as any).extractFromCookiesDB() // then: garbage was rejected, loop continued to the real token expect(copyAndExtractSpy).toHaveBeenCalledTimes(2) expect(results).toEqual([{ token: realToken, accountType: 'work', accountTypeKnown: true }]) getPathsSpy.mockRestore() copyAndExtractSpy.mockRestore() cleanup() }) // Regression for #163: browser-sourced tokens carry accountTypeKnown=false so that // the auth command can probe both endpoints. The extraction loop must preserve the // flag and must not dedupe an unknown-type browser token by its guessed accountType. it('propagates accountTypeKnown=false for browser-sourced tokens', async () => { // given: a browser Cookies path returning a valid token, guessed as work const browserPath = join(workDir, 'Chrome', 'Default', 'Cookies') mkdirSync(join(workDir, 'Chrome', 'Default'), { recursive: true }) writeFileSync(browserPath, '') const winExtractor = new TeamsTokenExtractor('win32') const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([ { path: browserPath, accountType: 'work', accountTypeKnown: false }, ]) const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract').mockResolvedValue(mockToken) // when const results = await (winExtractor as any).extractFromCookiesDB() // then: the flag is passed through so callers can probe expect(results).toEqual([{ token: mockToken, accountType: 'work', accountTypeKnown: false }]) getPathsSpy.mockRestore() copyAndExtractSpy.mockRestore() cleanup() }) // Regression for #163: when a desktop path (known=true) for 'work' already succeeded, // a subsequent browser path (known=false) guessed as 'work' must still be explored — // it might be a personal account misguessed as work. The dedup only kicks in for // confidently labeled paths. it('does not skip unknown-type path just because a known-type same-label succeeded', async () => { const desktopPath = join(workDir, 'WV2Profile_tfw', 'Network', 'Cookies') const browserPath = join(workDir, 'Chrome', 'Default', 'Cookies') mkdirSync(join(workDir, 'WV2Profile_tfw', 'Network'), { recursive: true }) mkdirSync(join(workDir, 'Chrome', 'Default'), { recursive: true }) writeFileSync(desktopPath, '') writeFileSync(browserPath, '') const desktopToken = mockToken const browserToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJicm93c2VyIn0.different_signature_here_abc' const winExtractor = new TeamsTokenExtractor('win32') const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([ { path: desktopPath, accountType: 'work', accountTypeKnown: true }, { path: browserPath, accountType: 'work', accountTypeKnown: false }, ]) const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract') .mockResolvedValueOnce(desktopToken) .mockResolvedValueOnce(browserToken) // when const results = await (winExtractor as any).extractFromCookiesDB() // then: both tokens returned; browser token keeps accountTypeKnown=false for probing expect(results).toEqual([ { token: desktopToken, accountType: 'work', accountTypeKnown: true }, { token: browserToken, accountType: 'work', accountTypeKnown: false }, ]) getPathsSpy.mockRestore() copyAndExtractSpy.mockRestore() cleanup() }) it('dedupes identical tokens extracted from multiple paths', async () => { const path1 = join(workDir, 'Chrome', 'Default', 'Cookies') const path2 = join(workDir, 'Edge', 'Default', 'Cookies') mkdirSync(join(workDir, 'Chrome', 'Default'), { recursive: true }) mkdirSync(join(workDir, 'Edge', 'Default'), { recursive: true }) writeFileSync(path1, '') writeFileSync(path2, '') const winExtractor = new TeamsTokenExtractor('win32') const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([ { path: path1, accountType: 'work', accountTypeKnown: false }, { path: path2, accountType: 'work', accountTypeKnown: false }, ]) const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract').mockResolvedValue(mockToken) // when const results = await (winExtractor as any).extractFromCookiesDB() // then: only one result despite two paths returning the same token expect(results).toHaveLength(1) expect(results[0].token).toBe(mockToken) getPathsSpy.mockRestore() copyAndExtractSpy.mockRestore() cleanup() }) it('a missing path does not mark the account type as seen', async () => { // given: work account has Cookies missing but Network/Cookies present const workProfile = join(workDir, 'WV2Profile_tfw') const workNetworkDir = join(workProfile, 'Network') mkdirSync(workNetworkDir, { recursive: true }) const workCookies = join(workProfile, 'Cookies') const workNetworkCookies = join(workNetworkDir, 'Cookies') writeFileSync(workNetworkCookies, '') const winExtractor = new TeamsTokenExtractor('win32') const getPathsSpy = spyOn(winExtractor, 'getTeamsCookiesPaths').mockReturnValue([ { path: workCookies, accountType: 'work', accountTypeKnown: true }, { path: workNetworkCookies, accountType: 'work', accountTypeKnown: true }, ]) const copyAndExtractSpy = spyOn(winExtractor as any, 'copyAndExtract').mockResolvedValue(mockToken) // when const results = await (winExtractor as any).extractFromCookiesDB() // then: missing first path did not block the sibling; work token extracted expect(results).toHaveLength(1) expect(results[0].accountType).toBe('work') expect(copyAndExtractSpy).toHaveBeenCalledTimes(1) expect(copyAndExtractSpy).toHaveBeenCalledWith(workNetworkCookies) getPathsSpy.mockRestore() copyAndExtractSpy.mockRestore() cleanup() }) }) describe('copyAndExtract', () => { it('attempts to copy database to temp location', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const copyFileSpy = spyOn(darwinExtractor as any, 'copyDatabaseToTemp').mockReturnValue('/tmp/test-cookies') const extractSpy = spyOn(darwinExtractor as any, 'extractFromSQLite').mockResolvedValue('test_token') const cleanupSpy = spyOn(darwinExtractor as any, 'cleanupTempFile').mockImplementation(() => {}) const result = await (darwinExtractor as any).copyAndExtract('/path/to/Cookies') expect(copyFileSpy).toHaveBeenCalled() expect(extractSpy).toHaveBeenCalled() expect(cleanupSpy).toHaveBeenCalled() expect(result).toBe('test_token') copyFileSpy.mockRestore() extractSpy.mockRestore() cleanupSpy.mockRestore() }) it('returns null when copy fails (file locked)', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const copyFileSpy = spyOn(darwinExtractor as any, 'copyDatabaseToTemp').mockImplementation(() => { throw new Error('EBUSY: resource busy or locked') }) const result = await (darwinExtractor as any).copyAndExtract('/path/to/Cookies') expect(result).toBeNull() copyFileSpy.mockRestore() }) }) describe('decryption', () => { describe('decryptAESGCM', () => { it('returns null for invalid encrypted data', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const invalidData = Buffer.from('too_short') const key = Buffer.alloc(32, 0) const result = (darwinExtractor as any).decryptAESGCM(invalidData, key) expect(result).toBeNull() }) it('returns null when decryption fails', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const fakeEncrypted = Buffer.concat([ Buffer.from('v10'), Buffer.alloc(12, 1), Buffer.alloc(20, 2), Buffer.alloc(16, 3), ]) const key = Buffer.alloc(32, 0) const result = (darwinExtractor as any).decryptAESGCM(fakeEncrypted, key) expect(result).toBeNull() }) }) describe('getKeychainPassword (macOS)', () => { it('tries multiple keychain variants', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const execSyncSpy = spyOn(darwinExtractor as any, 'execSecurityCommand') .mockReturnValueOnce(null) .mockReturnValueOnce('test_password') const result = (darwinExtractor as any).getKeychainPassword() expect(execSyncSpy).toHaveBeenCalledTimes(2) expect(result).toBe('test_password') execSyncSpy.mockRestore() }) it('returns null when all keychain variants fail', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const execSyncSpy = spyOn(darwinExtractor as any, 'execSecurityCommand').mockReturnValue(null) const result = (darwinExtractor as any).getKeychainPassword() expect(result).toBeNull() execSyncSpy.mockRestore() }) }) }) describe('process management', () => { describe('isTeamsRunning', () => { it('returns true when Teams process is found', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const checkProcessRunningSpy = spyOn(darwinExtractor as any, 'checkProcessRunning').mockReturnValue(true) const result = await darwinExtractor.isTeamsRunning() expect(result).toBe(true) checkProcessRunningSpy.mockRestore() }) it('returns false when no Teams process is found', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const checkProcessRunningSpy = spyOn(darwinExtractor as any, 'checkProcessRunning').mockReturnValue(false) const result = await darwinExtractor.isTeamsRunning() expect(result).toBe(false) checkProcessRunningSpy.mockRestore() }) }) describe('getProcessName', () => { it('returns correct process name for macOS', () => { const darwinExtractor = new TeamsTokenExtractor('darwin') expect((darwinExtractor as any).getProcessName()).toBe('Microsoft Teams') }) it('returns correct process name for Windows', () => { const winExtractor = new TeamsTokenExtractor('win32') expect((winExtractor as any).getProcessName()).toBe('Teams.exe') }) it('returns correct process name for Linux', () => { const linuxExtractor = new TeamsTokenExtractor('linux') expect((linuxExtractor as any).getProcessName()).toBe('teams') }) }) }) describe('SQLite extraction', () => { it('returns null when database path does not exist', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const result = await (darwinExtractor as any).extractFromSQLite('/nonexistent/path') expect(result).toBeNull() }) it('returns null when extraction throws', async () => { const darwinExtractor = new TeamsTokenExtractor('darwin') const result = await (darwinExtractor as any).extractFromSQLite('/dev/null') expect(result).toBeNull() }) }) })