Recover stale authenticated CSRF cookies and distinguish interface fetch failures

This commit is contained in:
archipelago
2026-10-06 01:08:31 -04:00
parent 2fa82e4506
commit 7e11f78eb4
5 changed files with 181 additions and 22 deletions
+7 -2
View File
@@ -279,7 +279,11 @@
/>
</div>
</div>
<p v-if="physicalInterfaces.length === 0" class="text-sm text-white/50 text-center py-4">No physical interfaces detected</p>
<div v-if="interfacesError" role="status" class="flex items-center justify-between gap-3 text-sm text-white/60 py-2">
<span>{{ allInterfaces.length ? 'Couldn’t refresh interfaces. Showing last known information.' : 'Couldn’t load network interfaces.' }}</span>
<button class="glass-button rounded px-3 py-1.5 text-xs shrink-0" :disabled="interfacesRefreshing" @click="loadInterfaces">Retry</button>
</div>
<p v-else-if="physicalInterfaces.length === 0 && interfacesRes.data.value !== null" class="text-sm text-white/50 text-center py-4">No physical interfaces detected</p>
<p v-if="wifiRadioError" class="text-xs text-red-400">{{ wifiRadioError }}</p>
</div>
</template>
@@ -653,7 +657,8 @@ const interfacesRes = useCachedResource<NetworkInterface[]>({
return res.interfaces
},
})
const interfacesLoading = computed(() => interfacesRes.loadState.value === 'loading')
const interfacesError = computed(() => interfacesRes.error.value)
const interfacesLoading = computed(() => interfacesRes.loadState.value === 'loading' || (interfacesRes.loadState.value === 'idle' && interfacesRes.data.value === null))
const interfacesRefreshing = computed(() => interfacesRes.loadState.value === 'refreshing')
const allInterfaces = computed(() => interfacesRes.data.value ?? [])
const physicalInterfaces = computed(() => allInterfaces.value.filter(i => i.type === 'ethernet' || i.type === 'wifi'))
@@ -43,6 +43,32 @@ function mountServer(options: { renderTorServices?: boolean } = {}) {
}
describe('Server network refresh states', () => {
it('shows an interface fetch failure without claiming hardware is absent, then retries', async () => {
sessionStorage.clear()
let failed = true
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
if (request.method === 'network.list-interfaces') {
if (failed) return Promise.reject(new Error('CSRF token missing or invalid'))
return Promise.resolve({ interfaces: [{ name: 'wlp3s0', type: 'wifi', state: 'up', mac: '', ipv4: ['192.0.2.10'] }] })
}
return Promise.resolve({})
})
vi.mocked(rpcClient.vpnStatus).mockResolvedValue({ connected: false } as never)
vi.mocked(rpcClient.dnsStatus).mockResolvedValue({ provider: 'system', resolv_conf_servers: [], doh_enabled: false } as never)
vi.mocked(rpcClient.diskStatus).mockResolvedValue({ encrypted: false, warnings: [] } as never)
const wrapper = mountServer()
await flushPromises()
expect(wrapper.text()).toContain('Couldn’t load network interfaces.')
expect(wrapper.text()).not.toContain('No physical interfaces detected')
failed = false
const retry = wrapper.findAll('button').find(button => button.text() === 'Retry')!
await retry.trigger('click')
await flushPromises()
expect(wrapper.text()).toContain('wlp3s0')
expect(wrapper.text()).not.toContain('Couldn’t load network interfaces.')
wrapper.unmount()
sessionStorage.clear()
})
it('keeps network overview visible while refresh is pending', async () => {
vi.mocked(rpcClient.call).mockImplementation((request: { method: string }) => {
if (request.method === 'network.diagnostics') {