260 lines
10 KiB
TypeScript
260 lines
10 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { sessionFromPartitionMock, askForMediaAccessMock, getMediaAccessStatusMock } = vi.hoisted(
|
|
() => ({
|
|
sessionFromPartitionMock: vi.fn(),
|
|
askForMediaAccessMock: vi.fn(),
|
|
getMediaAccessStatusMock: vi.fn()
|
|
})
|
|
)
|
|
|
|
vi.mock('electron', () => ({
|
|
session: {
|
|
fromPartition: sessionFromPartitionMock
|
|
},
|
|
systemPreferences: {
|
|
askForMediaAccess: askForMediaAccessMock,
|
|
getMediaAccessStatus: getMediaAccessStatusMock
|
|
}
|
|
}))
|
|
|
|
vi.mock('./browser-manager', () => ({
|
|
browserManager: {
|
|
notifyPermissionDenied: vi.fn(),
|
|
handleGuestWillDownload: vi.fn()
|
|
}
|
|
}))
|
|
|
|
import { browserSessionRegistry } from './browser-session-registry'
|
|
import { setupClientHintsOverride } from './browser-session-ua'
|
|
import { ORCA_BROWSER_PARTITION } from '../../shared/constants'
|
|
|
|
describe('BrowserSessionRegistry', () => {
|
|
beforeEach(() => {
|
|
sessionFromPartitionMock.mockReset()
|
|
askForMediaAccessMock.mockReset()
|
|
getMediaAccessStatusMock.mockReset()
|
|
askForMediaAccessMock.mockResolvedValue(true)
|
|
getMediaAccessStatusMock.mockReturnValue('granted')
|
|
sessionFromPartitionMock.mockReturnValue({
|
|
setPermissionRequestHandler: vi.fn(),
|
|
setPermissionCheckHandler: vi.fn(),
|
|
setDisplayMediaRequestHandler: vi.fn(),
|
|
on: vi.fn(),
|
|
clearStorageData: vi.fn().mockResolvedValue(undefined),
|
|
clearCache: vi.fn().mockResolvedValue(undefined)
|
|
})
|
|
})
|
|
|
|
it('has a default profile on construction', () => {
|
|
const defaultProfile = browserSessionRegistry.getDefaultProfile()
|
|
expect(defaultProfile.id).toBe('default')
|
|
expect(defaultProfile.scope).toBe('default')
|
|
expect(defaultProfile.partition).toBe(ORCA_BROWSER_PARTITION)
|
|
})
|
|
|
|
it('allows the default partition', () => {
|
|
expect(browserSessionRegistry.isAllowedPartition(ORCA_BROWSER_PARTITION)).toBe(true)
|
|
})
|
|
|
|
it('rejects unknown partitions', () => {
|
|
expect(browserSessionRegistry.isAllowedPartition('persist:evil-partition')).toBe(false)
|
|
})
|
|
|
|
it('creates an isolated profile with a unique partition', () => {
|
|
const profile = browserSessionRegistry.createProfile('isolated', 'Test Isolated')
|
|
expect(profile).not.toBeNull()
|
|
expect(profile!.scope).toBe('isolated')
|
|
expect(profile!.partition).toMatch(/^persist:orca-browser-session-/)
|
|
expect(profile!.partition).not.toBe(ORCA_BROWSER_PARTITION)
|
|
expect(profile!.label).toBe('Test Isolated')
|
|
expect(profile!.source).toBeNull()
|
|
})
|
|
|
|
it('rejects creating a profile with scope default', () => {
|
|
const profile = browserSessionRegistry.createProfile('default', 'Sneaky')
|
|
expect(profile).toBeNull()
|
|
})
|
|
|
|
it('allows created profile partitions', () => {
|
|
const profile = browserSessionRegistry.createProfile('isolated', 'Allowed')
|
|
expect(profile).not.toBeNull()
|
|
expect(browserSessionRegistry.isAllowedPartition(profile!.partition)).toBe(true)
|
|
})
|
|
|
|
it('creates an imported profile', () => {
|
|
const profile = browserSessionRegistry.createProfile('imported', 'My Import')
|
|
expect(profile).not.toBeNull()
|
|
expect(profile!.scope).toBe('imported')
|
|
expect(profile!.partition).toMatch(/^persist:orca-browser-session-/)
|
|
})
|
|
|
|
it('resolves partition for a known profile', () => {
|
|
const profile = browserSessionRegistry.createProfile('isolated', 'Resolve Test')
|
|
expect(profile).not.toBeNull()
|
|
expect(browserSessionRegistry.resolvePartition(profile!.id)).toBe(profile!.partition)
|
|
})
|
|
|
|
it('resolves default partition for null/undefined profileId', () => {
|
|
expect(browserSessionRegistry.resolvePartition(null)).toBe(ORCA_BROWSER_PARTITION)
|
|
expect(browserSessionRegistry.resolvePartition(undefined)).toBe(ORCA_BROWSER_PARTITION)
|
|
})
|
|
|
|
it('resolves default partition for unknown profileId', () => {
|
|
expect(browserSessionRegistry.resolvePartition('nonexistent')).toBe(ORCA_BROWSER_PARTITION)
|
|
})
|
|
|
|
it('lists all profiles', () => {
|
|
const before = browserSessionRegistry.listProfiles().length
|
|
browserSessionRegistry.createProfile('isolated', 'List Test')
|
|
const after = browserSessionRegistry.listProfiles()
|
|
expect(after.length).toBe(before + 1)
|
|
})
|
|
|
|
it('updates profile source', () => {
|
|
const profile = browserSessionRegistry.createProfile('imported', 'Source Test')
|
|
expect(profile).not.toBeNull()
|
|
const updated = browserSessionRegistry.updateProfileSource(profile!.id, {
|
|
browserFamily: 'edge',
|
|
importedAt: Date.now()
|
|
})
|
|
expect(updated).not.toBeNull()
|
|
expect(updated!.source?.browserFamily).toBe('edge')
|
|
})
|
|
|
|
it('updates profile source with comet family', () => {
|
|
const profile = browserSessionRegistry.createProfile('imported', 'Comet Source Test')
|
|
expect(profile).not.toBeNull()
|
|
const updated = browserSessionRegistry.updateProfileSource(profile!.id, {
|
|
browserFamily: 'comet',
|
|
importedAt: Date.now()
|
|
})
|
|
expect(updated).not.toBeNull()
|
|
expect(updated!.source?.browserFamily).toBe('comet')
|
|
})
|
|
|
|
it('deletes a non-default profile', async () => {
|
|
const profile = browserSessionRegistry.createProfile('isolated', 'Delete Test')
|
|
expect(profile).not.toBeNull()
|
|
expect(browserSessionRegistry.isAllowedPartition(profile!.partition)).toBe(true)
|
|
const deleted = await browserSessionRegistry.deleteProfile(profile!.id)
|
|
expect(deleted).toBe(true)
|
|
expect(browserSessionRegistry.isAllowedPartition(profile!.partition)).toBe(false)
|
|
expect(browserSessionRegistry.getProfile(profile!.id)).toBeNull()
|
|
})
|
|
|
|
it('refuses to delete the default profile', async () => {
|
|
const deleted = await browserSessionRegistry.deleteProfile('default')
|
|
expect(deleted).toBe(false)
|
|
expect(browserSessionRegistry.getDefaultProfile()).not.toBeNull()
|
|
})
|
|
|
|
it('hydrates profiles from persisted data', () => {
|
|
const fakeProfile = {
|
|
id: '00000000-0000-0000-0000-000000000001',
|
|
scope: 'imported' as const,
|
|
partition: 'persist:orca-browser-session-00000000-0000-0000-0000-000000000001',
|
|
label: 'Hydrated',
|
|
source: { browserFamily: 'manual' as const, importedAt: 1000 }
|
|
}
|
|
browserSessionRegistry.hydrateFromPersisted([fakeProfile])
|
|
expect(browserSessionRegistry.getProfile('00000000-0000-0000-0000-000000000001')).not.toBeNull()
|
|
expect(browserSessionRegistry.isAllowedPartition(fakeProfile.partition)).toBe(true)
|
|
})
|
|
|
|
it('sets up session policies for new partitions', () => {
|
|
browserSessionRegistry.createProfile('isolated', 'Policy Test')
|
|
expect(sessionFromPartitionMock).toHaveBeenCalled()
|
|
const mockSession = sessionFromPartitionMock.mock.results[0]?.value
|
|
expect(mockSession?.setPermissionRequestHandler).toHaveBeenCalled()
|
|
expect(mockSession?.setPermissionCheckHandler).toHaveBeenCalled()
|
|
})
|
|
|
|
it('routes media permission requests through macOS TCC for isolated partitions', async () => {
|
|
// Why: verify the parallel fix to the default partition — isolated/imported
|
|
// profiles must also defer media permission checks to macOS instead of
|
|
// denying outright, otherwise pages inside them still hit NotAllowedError
|
|
// after the user grants Camera/Microphone to Orca.
|
|
browserSessionRegistry.createProfile('isolated', 'Media Test')
|
|
const mockSession = sessionFromPartitionMock.mock.results[0]?.value
|
|
const requestHandler = mockSession.setPermissionRequestHandler.mock.calls[0][0]
|
|
const checkHandler = mockSession.setPermissionCheckHandler.mock.calls[0][0]
|
|
|
|
const cb = vi.fn()
|
|
const guestWc = { id: 7, getURL: vi.fn(() => 'https://example.com/') }
|
|
requestHandler(guestWc, 'media', cb, { mediaTypes: ['video'] })
|
|
await vi.waitFor(() => expect(cb).toHaveBeenCalledWith(true))
|
|
|
|
expect(checkHandler(null, 'media', '', { mediaType: 'video' })).toBe(true)
|
|
expect(checkHandler(null, 'notifications', '', {})).toBe(false)
|
|
})
|
|
|
|
describe('setupClientHintsOverride', () => {
|
|
it('overrides sec-ch-ua headers for Edge UA', () => {
|
|
const onBeforeSendHeaders = vi.fn()
|
|
const mockSess = { webRequest: { onBeforeSendHeaders } } as never
|
|
const edgeUa =
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.6890.3 Safari/537.36 Edg/147.0.3210.5'
|
|
|
|
setupClientHintsOverride(mockSess, edgeUa)
|
|
|
|
expect(onBeforeSendHeaders).toHaveBeenCalledWith(
|
|
{ urls: ['https://*/*'] },
|
|
expect.any(Function)
|
|
)
|
|
|
|
const callback = vi.fn()
|
|
const listener = onBeforeSendHeaders.mock.calls[0][1]
|
|
listener(
|
|
{ requestHeaders: { 'sec-ch-ua': 'old', 'sec-ch-ua-full-version-list': 'old' } },
|
|
callback
|
|
)
|
|
const modified = callback.mock.calls[0][0].requestHeaders
|
|
expect(modified['sec-ch-ua']).toContain('Microsoft Edge')
|
|
expect(modified['sec-ch-ua']).toContain('"147"')
|
|
expect(modified['sec-ch-ua-full-version-list']).toContain('147.0.3210.5')
|
|
})
|
|
|
|
it('overrides sec-ch-ua headers for Chrome UA', () => {
|
|
const onBeforeSendHeaders = vi.fn()
|
|
const mockSess = { webRequest: { onBeforeSendHeaders } } as never
|
|
const chromeUa =
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.6890.3 Safari/537.36'
|
|
|
|
setupClientHintsOverride(mockSess, chromeUa)
|
|
|
|
const callback = vi.fn()
|
|
const listener = onBeforeSendHeaders.mock.calls[0][1]
|
|
listener({ requestHeaders: { 'sec-ch-ua': 'old' } }, callback)
|
|
const modified = callback.mock.calls[0][0].requestHeaders
|
|
expect(modified['sec-ch-ua']).toContain('Google Chrome')
|
|
expect(modified['sec-ch-ua']).not.toContain('Microsoft Edge')
|
|
})
|
|
|
|
it('does not register handler for non-Chrome UA', () => {
|
|
const onBeforeSendHeaders = vi.fn()
|
|
const mockSess = { webRequest: { onBeforeSendHeaders } } as never
|
|
|
|
setupClientHintsOverride(mockSess, 'Mozilla/5.0 (compatible; MSIE 10.0)')
|
|
|
|
expect(onBeforeSendHeaders).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('leaves non-Client-Hints headers unchanged', () => {
|
|
const onBeforeSendHeaders = vi.fn()
|
|
const mockSess = { webRequest: { onBeforeSendHeaders } } as never
|
|
setupClientHintsOverride(mockSess, 'Mozilla/5.0 Chrome/147.0.0.0 Safari/537.36')
|
|
|
|
const callback = vi.fn()
|
|
const listener = onBeforeSendHeaders.mock.calls[0][1]
|
|
listener(
|
|
{ requestHeaders: { Cookie: 'abc=123', 'sec-ch-ua': 'old', Accept: 'text/html' } },
|
|
callback
|
|
)
|
|
const modified = callback.mock.calls[0][0].requestHeaders
|
|
expect(modified.Cookie).toBe('abc=123')
|
|
expect(modified.Accept).toBe('text/html')
|
|
})
|
|
})
|
|
})
|