Merge ngit external-access PR 79ca68c1 into combined UAT candidate
Preserve current maintenance/session guards, Firewall UI and existing catalogs. Retain scoped guest access, publishing journeys and local Blossom integration. Normalize Blossom/router memory units to supported quadlet suffixes. Validation: 108 dashboard tests, 10 gateway policy tests, strict source catalog check. Integrated isolated backend qualification remains required before main.
This commit is contained in:
@@ -1,15 +1,49 @@
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import AIConnectionModal from '../AIConnectionModal.vue'
|
||||
import ReceiveBitcoinModal from '../ReceiveBitcoinModal.vue'
|
||||
import WalletScanModal from '../WalletScanModal.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
vi.mock('@/api/rpc-client', () => ({ rpcClient: { call: vi.fn() } }))
|
||||
vi.mock('../ReceiveBitcoinModal.vue', () => ({ default: { props: ['show', 'initialMethod'], template: '<div />' } }))
|
||||
vi.mock('../WalletScanModal.vue', () => ({ default: { props: ['show'], template: '<div />' } }))
|
||||
vi.mock('../ReceiveBitcoinModal.vue', () => ({ default: { props: ['show', 'initialMethod', 'manageHistory'], template: '<div />' } }))
|
||||
vi.mock('@/views/settings/RoutstrBudgetSection.vue', () => ({ default: { template: '<div />' } }))
|
||||
const state = () => ({ schema: 1, settings: { provider: 'auto', openai_model: '' }, claude_configured: false, openai_configured: false, local_ready: false, routstr_remaining_sats: 0 })
|
||||
function mountModal() { return mount(AIConnectionModal, { props: { show: false }, global: { stubs: { BaseModal: { props: ['show'], template: '<div v-if="show"><slot /></div>' } } } }) }
|
||||
function button(w: ReturnType<typeof mountModal>, label: string) { return w.findAll('button').find(b => b.text() === label)! }
|
||||
beforeEach(() => { vi.clearAllMocks(); vi.mocked(rpcClient.call).mockImplementation(async ({ method }) => method === 'system.settings.get' ? { value: state() } : {}) })
|
||||
describe('AI connection setup', () => {
|
||||
it('recognizes the existing Claude key and selects it without reading or rewriting the secret', async () => {
|
||||
vi.mocked(rpcClient.call).mockImplementation(async ({ method }) => method === 'system.settings.get' ? { value: { ...state(), claude_configured: true } } : {})
|
||||
const w = mountModal(); await w.setProps({ show: true }); await flushPromises()
|
||||
await button(w, 'Claude API').trigger('click')
|
||||
expect(w.text()).toContain('Your Claude API key from Settings is ready to use.')
|
||||
expect(w.find('#ai-connection-key').exists()).toBe(false)
|
||||
expect(button(w, 'Use Claude').attributes('disabled')).toBeUndefined()
|
||||
await w.get('form').trigger('submit'); await flushPromises()
|
||||
const writes = vi.mocked(rpcClient.call).mock.calls.map(([r]) => r).filter(r => r.method === 'system.settings.set')
|
||||
expect(writes.map(r => r.params)).toEqual([{ key: 'ai_provider', value: JSON.stringify({ provider: 'claude', openai_model: '' }) }])
|
||||
expect(w.emitted('configured')).toEqual([['claude', undefined]])
|
||||
await button(w, 'Change API key').trigger('click')
|
||||
expect((w.get('#ai-connection-key').element as HTMLInputElement).value).toBe('')
|
||||
await w.setProps({ show: false }); w.unmount()
|
||||
})
|
||||
|
||||
it('opens on Routstr with a top-up action without authorizing spending', async () => {
|
||||
const w = mountModal(); await w.setProps({ show: true }); await flushPromises()
|
||||
expect(button(w, 'Routstr · sats').attributes('aria-pressed')).toBe('true')
|
||||
expect(button(w, 'Top up with ecash').exists()).toBe(true)
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.every(([r]) => !['assistant.budget-set', 'system.settings.set'].includes(r.method))).toBe(true)
|
||||
await button(w, 'Top up with ecash').trigger('click')
|
||||
expect(w.findComponent(ReceiveBitcoinModal).props()).toMatchObject({ show: true, initialMethod: 'ecash', manageHistory: false })
|
||||
w.findComponent(ReceiveBitcoinModal).vm.$emit('scan'); await flushPromises()
|
||||
expect(w.findComponent(WalletScanModal).props('show')).toBe(true)
|
||||
expect(w.findComponent(ReceiveBitcoinModal).props('show')).toBe(false)
|
||||
w.findComponent(WalletScanModal).vm.$emit('close'); await flushPromises()
|
||||
expect(w.findComponent(ReceiveBitcoinModal).props('show')).toBe(true)
|
||||
w.unmount()
|
||||
})
|
||||
|
||||
it('detects absent configuration without treating a failed status query as missing keys', async () => {
|
||||
const w = mountModal()
|
||||
expect(await (w.vm as any).checkNeeded()).toBe(true)
|
||||
@@ -26,7 +60,7 @@ describe('AI connection setup', () => {
|
||||
await w.get('form').trigger('submit'); await flushPromises()
|
||||
const writes = vi.mocked(rpcClient.call).mock.calls.map(([r]) => r).filter(r => r.method === 'system.settings.set')
|
||||
expect(writes.map(r => r.params)).toEqual([{ key: 'openai_api_key', value: 'test-private-key' }, { key: 'ai_provider', value: JSON.stringify({ provider: 'openai', openai_model: 'test-chat-model' }) }])
|
||||
expect((w.get('#ai-connection-key').element as HTMLInputElement).value).toBe('')
|
||||
expect(w.find('#ai-connection-key').exists()).toBe(false)
|
||||
expect(w.emitted('configured')).toEqual([['openai', 'test-chat-model']])
|
||||
expect(JSON.stringify(w.emitted())).not.toContain('test-private-key')
|
||||
w.unmount()
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
import BaseModal from '../BaseModal.vue'
|
||||
|
||||
describe('BaseModal', () => {
|
||||
it('lets a parent flow own history while panels change', async () => {
|
||||
const push = vi.spyOn(window.history, 'pushState')
|
||||
const back = vi.spyOn(window.history, 'back')
|
||||
const wrapper = mount(BaseModal, { props: { show: false, title: 'Panel', manageHistory: false } })
|
||||
await wrapper.setProps({ show: true })
|
||||
await wrapper.setProps({ show: false })
|
||||
expect(push).not.toHaveBeenCalled()
|
||||
expect(back).not.toHaveBeenCalled()
|
||||
wrapper.unmount(); push.mockRestore(); back.mockRestore()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.style.overflow = ''
|
||||
})
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import SearchableAppSelect from '../SearchableAppSelect.vue'
|
||||
const options = [{ id: 'homeassistant', name: 'Home Assistant' }, { id: 'immich', name: 'Immich' }]
|
||||
describe('searchable app picker', () => {
|
||||
it('renders actual touchable options, filters them and selects only an offered app', async () => {
|
||||
const wrapper = mount(SearchableAppSelect, { props: { modelValue: '', options } })
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.findAll('[role="option"]')).toHaveLength(2)
|
||||
await wrapper.get('input').setValue('imm')
|
||||
expect(wrapper.findAll('[role="option"]')).toHaveLength(1)
|
||||
await wrapper.get('[role="option"]').trigger('click')
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([['immich']])
|
||||
expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
|
||||
})
|
||||
it('supports keyboard choice and escape without selecting arbitrary search text', async () => {
|
||||
const wrapper = mount(SearchableAppSelect, { props: { modelValue: '', options } })
|
||||
await wrapper.get('button').trigger('keydown', { key: 'ArrowDown' })
|
||||
await wrapper.get('input').setValue('home')
|
||||
await wrapper.get('input').trigger('keydown', { key: 'Enter' })
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([['homeassistant']])
|
||||
await wrapper.get('button').trigger('click')
|
||||
await wrapper.get('input').setValue('unknown')
|
||||
await wrapper.get('input').trigger('keydown', { key: 'Enter' })
|
||||
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
||||
await wrapper.get('input').trigger('keydown', { key: 'Escape' })
|
||||
expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import WebsiteArchiveSharing from '../WebsiteArchiveSharing.vue'
|
||||
|
||||
describe('archived website sharing consent', () => {
|
||||
it('requires fresh approval after the reviewed snapshot changes', async () => {
|
||||
const wrapper = mount(WebsiteArchiveSharing, { props: { publication: { html: '<h1>Reviewed</h1>' }, archive: { sha256: 'a'.repeat(64), size: 17 } } })
|
||||
expect(wrapper.get('button').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.get('iframe').attributes('sandbox')).toBe('')
|
||||
expect(wrapper.get('iframe').attributes('srcdoc')).toContain('<h1>Reviewed</h1>')
|
||||
await wrapper.get('input').setValue(true)
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.emitted('change')).toEqual([[true]])
|
||||
await wrapper.setProps({ publication: { html: '<h1>Different</h1>' } })
|
||||
expect(wrapper.get('button').attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.emitted('change')).toHaveLength(1)
|
||||
})
|
||||
it('keeps removal available without new approval and exposes only the selected hash', async () => {
|
||||
const hash = 'a'.repeat(64)
|
||||
const wrapper = mount(WebsiteArchiveSharing, { props: { publication: { html: 'public', public_archive: hash }, address: 'https://example.com/' } })
|
||||
expect(wrapper.text()).toContain('https://example.com/'+hash)
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.emitted('change')).toEqual([[false]])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user