test: achieve 80%+ branch/function coverage on frontend logic (E2E-03)

515 tests across 38 files. Branch coverage 88%, function coverage 83%
on testable logic (stores, composables, api, utils, services, router).

New test files: websocket, useLoginSounds, useMobileBackButton,
useControllerNav, routes. Extended: rpc-client (99.5%), container store
(100%). Fixed: useNavSounds AudioContext mock, type errors across tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-11 17:18:37 +00:00
co-authored by Claude Opus 4.6
parent 0b6068f452
commit 1697af725b
14 changed files with 2161 additions and 2 deletions
@@ -0,0 +1,163 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ref, type Ref } from 'vue'
import { setActivePinia, createPinia } from 'pinia'
vi.mock('@/api/rpc-client', () => ({
rpcClient: {
call: vi.fn(),
},
}))
vi.mock('@/api/filebrowser-client', () => ({
fileBrowserClient: {
login: vi.fn(),
isAuthenticated: false,
getUsage: vi.fn(),
listDirectory: vi.fn(),
readFileAsText: vi.fn(),
},
}))
import { ContextBroker } from '../contextBroker'
import { useAIPermissionsStore } from '@/stores/aiPermissions'
describe('ContextBroker', () => {
let broker: ContextBroker
let iframeRef: Ref<HTMLIFrameElement | null>
let mockPostMessage: ReturnType<typeof vi.fn>
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
mockPostMessage = vi.fn()
iframeRef = ref<HTMLIFrameElement | null>({
contentWindow: {
postMessage: mockPostMessage,
},
} as unknown as HTMLIFrameElement)
broker = new ContextBroker(iframeRef, 'http://localhost:8100')
})
it('creates with correct allowed origin', () => {
expect(broker).toBeDefined()
})
it('start registers message listener', () => {
const addSpy = vi.spyOn(window, 'addEventListener')
broker.start()
expect(addSpy).toHaveBeenCalledWith('message', expect.any(Function))
broker.stop()
addSpy.mockRestore()
})
it('stop removes message listener', () => {
const removeSpy = vi.spyOn(window, 'removeEventListener')
broker.start()
broker.stop()
expect(removeSpy).toHaveBeenCalledWith('message', expect.any(Function))
removeSpy.mockRestore()
})
it('sendTheme sends theme response to iframe', () => {
broker.sendTheme()
expect(mockPostMessage).toHaveBeenCalledWith(
{ type: 'theme:response', theme: { accent: '#fb923c', mode: 'dark' } },
expect.any(String),
)
})
it('sendPermissionsUpdate sends enabled categories to iframe', () => {
const perms = useAIPermissionsStore()
perms.toggle('apps')
perms.toggle('system')
broker.sendPermissionsUpdate()
expect(mockPostMessage).toHaveBeenCalledWith(
expect.objectContaining({
type: 'permissions:update',
categories: expect.arrayContaining(['apps', 'system']),
}),
expect.any(String),
)
})
it('does not post when iframe has no contentWindow', () => {
iframeRef.value = null
broker.sendTheme()
expect(mockPostMessage).not.toHaveBeenCalled()
})
describe('path validation', () => {
// Access private method via prototype
const isPathAllowed = (path: string) => {
return (broker as unknown as { isPathAllowed: (p: string) => boolean }).isPathAllowed(path)
}
it('allows paths in /var/lib/archipelago/', () => {
expect(isPathAllowed('/var/lib/archipelago/bitcoin/data.db')).toBe(true)
})
it('allows paths in /var/log/', () => {
expect(isPathAllowed('/var/log/syslog')).toBe(true)
})
it('rejects paths outside allowed directories', () => {
expect(isPathAllowed('/etc/shadow')).toBe(false)
expect(isPathAllowed('/root/.ssh/id_rsa')).toBe(false)
})
it('rejects paths with sensitive patterns', () => {
expect(isPathAllowed('/var/lib/archipelago/secret.key')).toBe(false)
expect(isPathAllowed('/var/lib/archipelago/password.txt')).toBe(false)
expect(isPathAllowed('/var/lib/archipelago/wallet.dat')).toBe(false)
expect(isPathAllowed('/var/lib/archipelago/.env')).toBe(false)
expect(isPathAllowed('/var/lib/archipelago/admin.macaroon')).toBe(false)
})
it('strips path traversal sequences before checking', () => {
// After stripping ../, path becomes /var/lib/archipelago/etc/passwd (still in allowed dir)
expect(isPathAllowed('/var/lib/archipelago/../../etc/passwd')).toBe(true)
// Path outside allowed dir is rejected even without traversal
expect(isPathAllowed('/etc/passwd')).toBe(false)
// Sensitive pattern inside allowed dir is still blocked
expect(isPathAllowed('/var/lib/archipelago/../../etc/password')).toBe(false)
})
})
describe('log redaction', () => {
const redact = (line: string) => {
return (ContextBroker as unknown as { redactLogLine: (l: string) => string }).redactLogLine(line)
}
it('redacts password= patterns', () => {
const result = redact('rpcpassword=mysecretpassword123')
expect(result).not.toContain('mysecretpassword123')
expect(result).toContain('[REDACTED]')
})
it('redacts token= patterns', () => {
const result = redact('token=abc123def456')
expect(result).not.toContain('abc123def456')
})
it('redacts long hex strings (private keys)', () => {
const hexKey = 'a'.repeat(64)
const result = redact(`key: ${hexKey}`)
expect(result).not.toContain(hexKey)
expect(result).toContain('[REDACTED_KEY]')
})
it('redacts long base64 strings (macaroons/tokens)', () => {
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/AABB'
const result = redact(`macaroon: ${b64}`)
expect(result).toContain('[REDACTED')
})
it('preserves non-sensitive log lines', () => {
const line = '2026-03-11 INFO: Bitcoin block height: 841234'
expect(redact(line)).toBe(line)
})
})
})