chore: clean cache

This commit is contained in:
GitHub Actions
2025-12-11 18:17:21 +00:00
parent b4dd1efe3c
commit 65d837a13f
646 changed files with 0 additions and 129862 deletions

View File

@@ -1,555 +0,0 @@
import { AxiosError } from 'axios'
import { screen, waitFor, act, cleanup, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient } from '@tanstack/react-query'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import CrowdSecConfig from '../CrowdSecConfig'
import * as securityApi from '../../api/security'
import * as crowdsecApi from '../../api/crowdsec'
import * as presetsApi from '../../api/presets'
import * as backupsApi from '../../api/backups'
import * as settingsApi from '../../api/settings'
import { CROWDSEC_PRESETS } from '../../data/crowdsecPresets'
import { renderWithQueryClient, createTestQueryClient } from '../../test-utils/renderWithQueryClient'
import { toast } from '../../utils/toast'
import * as exportUtils from '../../utils/crowdsecExport'
vi.mock('../../api/security')
vi.mock('../../api/crowdsec')
vi.mock('../../api/presets')
vi.mock('../../api/backups')
vi.mock('../../api/settings')
vi.mock('../../utils/crowdsecExport', () => ({
buildCrowdsecExportFilename: vi.fn(() => 'crowdsec-default.tar.gz'),
promptCrowdsecFilename: vi.fn(() => 'crowdsec.tar.gz'),
downloadCrowdsecExport: vi.fn(),
}))
vi.mock('../../utils/toast', () => ({
toast: {
success: vi.fn(),
error: vi.fn(),
info: vi.fn(),
},
}))
const baseStatus = {
cerberus: { enabled: true },
crowdsec: { enabled: true, mode: 'local' as const, api_url: '' },
waf: { enabled: true, mode: 'enabled' as const },
rate_limit: { enabled: true },
acl: { enabled: true },
}
const disabledStatus = {
...baseStatus,
crowdsec: { ...baseStatus.crowdsec, enabled: true, mode: 'disabled' as const },
}
const presetFromCatalog = CROWDSEC_PRESETS[0]
const axiosError = (status: number, message: string, data?: Record<string, unknown>) =>
new AxiosError(message, undefined, undefined, undefined, {
status,
statusText: String(status),
headers: {},
config: {},
data: data ?? { error: message },
} as never)
const defaultFileList = ['acquis.yaml', 'collections.yaml']
const renderPage = async (client?: QueryClient) => {
const result = renderWithQueryClient(<CrowdSecConfig />, { client })
await waitFor(() => screen.getByText('CrowdSec Configuration'))
return result
}
describe('CrowdSecConfig coverage', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(baseStatus)
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: defaultFileList })
vi.mocked(crowdsecApi.readCrowdsecFile).mockResolvedValue({ content: 'file-content' })
vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue(undefined)
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValue({ decisions: [] })
vi.mocked(crowdsecApi.banIP).mockResolvedValue(undefined)
vi.mocked(crowdsecApi.unbanIP).mockResolvedValue(undefined)
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockResolvedValue(new Blob(['data']))
vi.mocked(crowdsecApi.importCrowdsecConfig).mockResolvedValue(undefined)
vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValue({
presets: [
{
slug: presetFromCatalog.slug,
title: presetFromCatalog.title,
summary: presetFromCatalog.description,
source: 'hub',
requires_hub: false,
available: true,
cached: false,
cache_key: 'cache-123',
etag: 'etag-123',
retrieved_at: '2024-01-01T00:00:00Z',
},
],
})
vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
status: 'pulled',
slug: presetFromCatalog.slug,
preview: presetFromCatalog.content,
cache_key: 'cache-123',
etag: 'etag-123',
retrieved_at: '2024-01-01T00:00:00Z',
source: 'hub',
})
vi.mocked(presetsApi.applyCrowdsecPreset).mockResolvedValue({
status: 'applied',
backup: '/tmp/backup.tar.gz',
reload_hint: true,
used_cscli: true,
cache_key: 'cache-123',
slug: presetFromCatalog.slug,
})
vi.mocked(presetsApi.getCrowdsecPresetCache).mockResolvedValue({
preview: 'cached-preview',
cache_key: 'cache-123',
etag: 'etag-123',
})
vi.mocked(backupsApi.createBackup).mockResolvedValue({ filename: 'backup.tar.gz' })
vi.mocked(settingsApi.updateSetting).mockResolvedValue()
})
it('renders loading and error boundaries', async () => {
vi.mocked(securityApi.getSecurityStatus).mockReturnValue(new Promise(() => {}))
renderWithQueryClient(<CrowdSecConfig />)
expect(await screen.findByText('Loading CrowdSec configuration...')).toBeInTheDocument()
cleanup()
vi.mocked(securityApi.getSecurityStatus).mockRejectedValue(new Error('boom'))
renderWithQueryClient(<CrowdSecConfig />)
expect(await screen.findByText(/Failed to load security status/)).toBeInTheDocument()
})
it('handles missing status and missing crowdsec sections', async () => {
vi.mocked(securityApi.getSecurityStatus).mockRejectedValueOnce(new Error('data is undefined'))
renderWithQueryClient(<CrowdSecConfig />)
expect(await screen.findByText(/Failed to load security status/)).toBeInTheDocument()
cleanup()
vi.mocked(securityApi.getSecurityStatus).mockResolvedValueOnce({ cerberus: { enabled: true } } as never)
renderWithQueryClient(<CrowdSecConfig />)
expect(await screen.findByText('CrowdSec configuration not found in security status')).toBeInTheDocument()
})
it('renders disabled mode message and bans control disabled', async () => {
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(disabledStatus)
await renderPage(createTestQueryClient())
expect(screen.getByText('Enable CrowdSec to manage banned IPs')).toBeInTheDocument()
expect(screen.getByRole('button', { name: /Ban IP/ })).toBeDisabled()
})
it('toggles mode success and error', async () => {
await renderPage()
const toggle = screen.getByTestId('crowdsec-mode-toggle')
await userEvent.click(toggle)
await waitFor(() => expect(settingsApi.updateSetting).toHaveBeenCalledWith('security.crowdsec.mode', 'disabled', 'security', 'string'))
expect(toast.success).toHaveBeenCalledWith('CrowdSec disabled')
vi.mocked(settingsApi.updateSetting).mockRejectedValueOnce(new Error('nope'))
await userEvent.click(toggle)
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('nope'))
})
it('guards import without a file and shows error on import failure', async () => {
await renderPage()
const importBtn = screen.getByTestId('import-btn')
await userEvent.click(importBtn)
expect(backupsApi.createBackup).not.toHaveBeenCalled()
const fileInput = screen.getByTestId('import-file') as HTMLInputElement
const file = new File(['data'], 'cfg.tar.gz')
await userEvent.upload(fileInput, file)
vi.mocked(crowdsecApi.importCrowdsecConfig).mockRejectedValueOnce(new Error('bad import'))
await userEvent.click(importBtn)
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('bad import'))
})
it('imports configuration after creating a backup', async () => {
await renderPage()
const fileInput = screen.getByTestId('import-file') as HTMLInputElement
await userEvent.upload(fileInput, new File(['data'], 'cfg.tar.gz'))
await userEvent.click(screen.getByTestId('import-btn'))
await waitFor(() => expect(backupsApi.createBackup).toHaveBeenCalled())
await waitFor(() => expect(crowdsecApi.importCrowdsecConfig).toHaveBeenCalled())
})
it('exports configuration success and failure', async () => {
await renderPage()
await userEvent.click(screen.getByRole('button', { name: 'Export' }))
await waitFor(() => expect(crowdsecApi.exportCrowdsecConfig).toHaveBeenCalled())
expect(exportUtils.downloadCrowdsecExport).toHaveBeenCalled()
expect(toast.success).toHaveBeenCalledWith('CrowdSec configuration exported')
vi.mocked(exportUtils.promptCrowdsecFilename).mockReturnValueOnce('crowdsec.tar.gz')
vi.mocked(crowdsecApi.exportCrowdsecConfig).mockRejectedValueOnce(new Error('fail'))
await userEvent.click(screen.getByRole('button', { name: 'Export' }))
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Failed to export CrowdSec configuration'))
})
it('auto-selects first preset and pulls preview', async () => {
await renderPage()
const select = screen.getByTestId('preset-select') as HTMLSelectElement
expect(select.value).toBe(presetFromCatalog.slug)
await waitFor(() => expect(presetsApi.pullCrowdsecPreset).toHaveBeenCalledWith(presetFromCatalog.slug))
const previewText = screen.getByTestId('preset-preview').textContent?.replace(/\s+/g, ' ')
expect(previewText).toContain('crowdsecurity/http-cve')
expect(screen.getByTestId('preset-meta')).toHaveTextContent('cache-123')
})
it('handles pull validation, hub unavailable, and generic errors', async () => {
vi.mocked(presetsApi.pullCrowdsecPreset).mockRejectedValueOnce(axiosError(400, 'slug invalid', { error: 'slug invalid' }))
await renderPage()
expect(await screen.findByTestId('preset-validation-error')).toHaveTextContent('slug invalid')
vi.mocked(presetsApi.pullCrowdsecPreset).mockRejectedValueOnce(axiosError(503, 'hub down', { error: 'hub down' }))
await userEvent.click(screen.getByText('Pull Preview'))
await waitFor(() => expect(screen.getByTestId('preset-hub-unavailable')).toBeInTheDocument())
vi.mocked(presetsApi.pullCrowdsecPreset).mockRejectedValueOnce(axiosError(500, 'boom', { error: 'boom' }))
await userEvent.click(screen.getByText('Pull Preview'))
await waitFor(() => expect(screen.getByTestId('preset-status')).toHaveTextContent('boom'))
})
it('loads cached preview and reports cache errors', async () => {
vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValueOnce({
presets: [
{
slug: presetFromCatalog.slug,
title: presetFromCatalog.title,
summary: presetFromCatalog.description,
source: 'hub',
requires_hub: false,
available: true,
cached: true,
cache_key: 'cache-123',
etag: 'etag-123',
retrieved_at: '2024-01-01T00:00:00Z',
},
],
})
await renderPage()
await userEvent.click(screen.getByText('Pull Preview'))
await waitFor(() => {
const preview = screen.getByTestId('preset-preview').textContent?.replace(/\s+/g, ' ')
expect(preview).toContain('crowdsecurity/http-cve')
})
await userEvent.click(screen.getByText('Load cached preview'))
await waitFor(() => expect(screen.getByTestId('preset-preview')).toHaveTextContent('cached-preview'))
vi.mocked(presetsApi.getCrowdsecPresetCache).mockRejectedValueOnce(axiosError(500, 'cache-miss'))
await userEvent.click(screen.getByText('Load cached preview'))
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('cache-miss'))
})
it('sets apply info on backend success', async () => {
await renderPage()
await userEvent.click(screen.getByTestId('apply-preset-btn'))
await waitFor(() => expect(screen.getByTestId('preset-apply-info')).toHaveTextContent('Backup: /tmp/backup.tar.gz'))
})
it('falls back to local apply on 501 and covers validation/hub/offline branches', async () => {
vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue({})
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(501, 'not implemented'))
await renderPage()
const applyBtn = screen.getByTestId('apply-preset-btn')
await userEvent.click(applyBtn)
await waitFor(() => expect(toast.info).toHaveBeenCalledWith('Preset apply is not available on the server; applying locally instead'))
await waitFor(() => expect(crowdsecApi.writeCrowdsecFile).toHaveBeenCalled())
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(400, 'bad', { error: 'validation failed' }))
await userEvent.click(applyBtn)
await waitFor(() => expect(screen.getByTestId('preset-validation-error')).toHaveTextContent('validation failed'))
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(503, 'hub'))
await userEvent.click(applyBtn)
await waitFor(() => expect(screen.getByTestId('preset-hub-unavailable')).toBeInTheDocument())
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(500, 'not cached', { error: 'Pull the preset first' }))
await userEvent.click(applyBtn)
await waitFor(() => expect(screen.getByTestId('preset-validation-error')).toHaveTextContent('Preset must be pulled'))
})
it('records backup info on apply failure and generic errors', async () => {
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(500, 'failed', { error: 'boom', backup: '/tmp/backup' }))
await renderPage()
await userEvent.click(screen.getByTestId('apply-preset-btn'))
await waitFor(() => expect(screen.getByTestId('preset-apply-info')).toHaveTextContent('/tmp/backup'))
cleanup()
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(new Error('unexpected'))
await renderPage()
await userEvent.click(screen.getByTestId('apply-preset-btn'))
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Failed to apply preset'))
})
it('disables apply when hub is unavailable for hub-only preset', async () => {
vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValueOnce({
presets: [
{
slug: 'hub-only',
title: 'Hub Only',
summary: 'needs hub',
source: 'hub',
requires_hub: true,
available: true,
cached: true,
cache_key: 'cache-hub',
etag: 'etag-hub',
},
],
})
vi.mocked(presetsApi.pullCrowdsecPreset).mockRejectedValueOnce(axiosError(503, 'hub'))
await renderPage()
await waitFor(() => expect(screen.getByTestId('preset-hub-unavailable')).toBeInTheDocument())
expect((screen.getByTestId('apply-preset-btn') as HTMLButtonElement).disabled).toBe(true)
})
it('guards local apply prerequisites and succeeds when content exists', async () => {
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValueOnce({ files: [] })
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(501, 'not implemented'))
await renderPage()
await userEvent.click(screen.getByTestId('apply-preset-btn'))
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Select a configuration file to apply the preset'))
cleanup()
vi.mocked(toast.error).mockClear()
vi.mocked(crowdsecApi.listCrowdsecFiles).mockResolvedValue({ files: ['acquis.yaml'] })
vi.mocked(presetsApi.listCrowdsecPresets).mockResolvedValue({
presets: [
{
slug: 'custom-empty',
title: 'Empty',
summary: 'empty preset',
source: 'hub',
requires_hub: false,
available: true,
cached: false,
cache_key: 'cache-empty',
etag: 'etag-empty',
},
],
})
vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
status: 'pulled',
slug: 'custom-empty',
preview: '',
cache_key: 'cache-empty',
})
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(501, 'not implemented'))
await renderPage()
await userEvent.selectOptions(screen.getByTestId('crowdsec-file-select'), 'acquis.yaml')
await userEvent.click(screen.getByTestId('apply-preset-btn'))
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Preset preview is unavailable; retry pulling before applying'))
cleanup()
vi.mocked(toast.error).mockClear()
vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
status: 'pulled',
slug: presetFromCatalog.slug,
preview: 'content',
cache_key: 'cache-123',
})
vi.mocked(presetsApi.applyCrowdsecPreset).mockRejectedValueOnce(axiosError(501, 'not implemented'))
vi.mocked(crowdsecApi.writeCrowdsecFile).mockResolvedValue({})
await renderPage()
await userEvent.selectOptions(screen.getByTestId('crowdsec-file-select'), 'acquis.yaml')
await userEvent.click(screen.getByTestId('apply-preset-btn'))
await waitFor(() => expect(crowdsecApi.writeCrowdsecFile).toHaveBeenCalled())
})
it('reads, edits, saves, and closes files', async () => {
await renderPage()
await userEvent.selectOptions(screen.getByTestId('crowdsec-file-select'), 'acquis.yaml')
await waitFor(() => expect(crowdsecApi.readCrowdsecFile).toHaveBeenCalledWith('acquis.yaml'))
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
expect(textarea.value).toBe('file-content')
await userEvent.clear(textarea)
await userEvent.type(textarea, 'updated')
await userEvent.click(screen.getByText('Save'))
await waitFor(() => expect(backupsApi.createBackup).toHaveBeenCalled())
await waitFor(() => expect(crowdsecApi.writeCrowdsecFile).toHaveBeenCalledWith('acquis.yaml', 'updated'))
await userEvent.click(screen.getByText('Close'))
expect((screen.getByTestId('crowdsec-file-select') as HTMLSelectElement).value).toBe('')
})
it('shows decisions table, handles loading/error/empty states, and unban errors', async () => {
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(disabledStatus)
await renderPage()
expect(screen.getByText('Enable CrowdSec to manage banned IPs')).toBeInTheDocument()
cleanup()
vi.mocked(securityApi.getSecurityStatus).mockResolvedValue(baseStatus)
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockReturnValue(new Promise(() => {}))
await renderPage()
expect(screen.getByText('Loading banned IPs...')).toBeInTheDocument()
cleanup()
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockRejectedValueOnce(new Error('decisions'))
await renderPage()
expect(await screen.findByText('Failed to load banned IPs')).toBeInTheDocument()
cleanup()
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValueOnce({ decisions: [] })
await renderPage()
expect(await screen.findByText('No banned IPs')).toBeInTheDocument()
cleanup()
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValueOnce({
decisions: [
{ id: '1', ip: '1.1.1.1', reason: 'bot', duration: '24h', created_at: '2024-01-01T00:00:00Z', source: 'manual' },
],
})
await renderPage()
expect(await screen.findByText('1.1.1.1')).toBeInTheDocument()
vi.mocked(crowdsecApi.unbanIP).mockRejectedValueOnce(new Error('unban fail'))
await userEvent.click(screen.getAllByText('Unban')[0])
const confirmModal = screen.getByText('Confirm Unban').closest('div') as HTMLElement
await userEvent.click(within(confirmModal).getByRole('button', { name: 'Unban' }))
await waitFor(() => expect(toast.error).toHaveBeenCalledWith('unban fail'))
})
it('bans and unbans IPs with overlay messaging', async () => {
vi.mocked(crowdsecApi.listCrowdsecDecisions).mockResolvedValue({
decisions: [
{ id: '1', ip: '1.1.1.1', reason: 'bot', duration: '24h', created_at: '2024-01-01T00:00:00Z', source: 'manual' },
],
})
await renderPage()
await userEvent.click(screen.getByRole('button', { name: /Ban IP/ }))
const banModal = screen.getByText('Ban IP Address').closest('div') as HTMLElement
const ipInput = within(banModal).getByPlaceholderText('192.168.1.100') as HTMLInputElement
await userEvent.type(ipInput, '2.2.2.2')
await userEvent.click(within(banModal).getByRole('button', { name: 'Ban IP' }))
await waitFor(() => expect(crowdsecApi.banIP).toHaveBeenCalledWith('2.2.2.2', '24h', ''))
// keep ban pending to assert overlay message
let resolveBan: (() => void) | undefined
vi.mocked(crowdsecApi.banIP).mockImplementationOnce(
() =>
new Promise<void>((resolve) => {
resolveBan = () => resolve()
}),
)
await userEvent.click(screen.getByRole('button', { name: /Ban IP/ }))
const banModalSecond = screen.getByText('Ban IP Address').closest('div') as HTMLElement
await userEvent.type(within(banModalSecond).getByPlaceholderText('192.168.1.100'), '3.3.3.3')
await userEvent.click(within(banModalSecond).getByRole('button', { name: 'Ban IP' }))
expect(await screen.findByText('Guardian raises shield...')).toBeInTheDocument()
resolveBan?.()
vi.mocked(crowdsecApi.unbanIP).mockImplementationOnce(() => new Promise(() => {}))
const unbanButtons = await screen.findAllByText('Unban')
await userEvent.click(unbanButtons[0])
const confirmDialog = screen.getByText('Confirm Unban').closest('div') as HTMLElement
await userEvent.click(within(confirmDialog).getByRole('button', { name: 'Unban' }))
expect(await screen.findByText('Guardian lowers shield...')).toBeInTheDocument()
})
it('shows overlay messaging for preset pull, apply, import, write, and mode updates', async () => {
// pull pending
vi.mocked(presetsApi.pullCrowdsecPreset).mockImplementation(() => new Promise(() => {}))
await renderPage()
await userEvent.click(screen.getByText('Pull Preview'))
expect(await screen.findByText('Fetching preset...')).toBeInTheDocument()
cleanup()
vi.mocked(presetsApi.pullCrowdsecPreset).mockReset()
vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValue({
status: 'pulled',
slug: presetFromCatalog.slug,
preview: presetFromCatalog.content,
cache_key: 'cache-123',
})
// apply pending
vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValueOnce({
status: 'pulled',
slug: presetFromCatalog.slug,
preview: presetFromCatalog.content,
cache_key: 'cache-123',
})
let resolveApply: (() => void) | undefined
vi.mocked(presetsApi.applyCrowdsecPreset).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveApply = () => resolve({ status: 'applied', cache_key: 'cache-123' } as never)
}),
)
await renderPage()
await userEvent.click(screen.getAllByTestId('apply-preset-btn')[0])
expect(await screen.findByText('Loading preset...')).toBeInTheDocument()
resolveApply?.()
cleanup()
// import pending
vi.mocked(presetsApi.pullCrowdsecPreset).mockResolvedValueOnce({
status: 'pulled',
slug: presetFromCatalog.slug,
preview: presetFromCatalog.content,
cache_key: 'cache-123',
})
let resolveImport: (() => void) | undefined
vi.mocked(crowdsecApi.importCrowdsecConfig).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveImport = () => resolve({})
}),
)
const { queryClient } = await renderPage(createTestQueryClient())
await waitFor(() => expect(screen.getByTestId('preset-preview')).toBeInTheDocument())
const fileInput = screen.getByTestId('import-file') as HTMLInputElement
await userEvent.upload(fileInput, new File(['data'], 'cfg.tar.gz'))
await userEvent.click(screen.getByTestId('import-btn'))
expect(await screen.findByText('Summoning the guardian...')).toBeInTheDocument()
resolveImport?.()
await act(async () => queryClient.cancelQueries())
cleanup()
// write pending
let resolveWrite: (() => void) | undefined
vi.mocked(crowdsecApi.writeCrowdsecFile).mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveWrite = () => resolve({})
}),
)
await renderPage()
await waitFor(() => expect(screen.getByTestId('preset-preview')).toBeInTheDocument())
await userEvent.selectOptions(screen.getByTestId('crowdsec-file-select'), 'acquis.yaml')
const textarea = screen.getByRole('textbox') as HTMLTextAreaElement
await userEvent.type(textarea, 'x')
await userEvent.click(screen.getByText('Save'))
expect(await screen.findByText('Guardian inscribes...')).toBeInTheDocument()
resolveWrite?.()
cleanup()
// mode update pending
vi.mocked(settingsApi.updateSetting).mockImplementationOnce(() => new Promise(() => {}))
await renderPage()
await userEvent.click(screen.getByTestId('crowdsec-mode-toggle'))
expect(await screen.findByText('Three heads turn...')).toBeInTheDocument()
})
})