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:
co-authored by
Claude Opus 4.6
parent
0b6068f452
commit
1697af725b
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user