Files
EvoFirewall/apps/web/test/f2.tsx
T
Denozordec 9fa6269174
quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
feat: harden firewall policies telemetry and operations
Implement reliable agent apply, telemetry v2, analytics retention, guarded rollouts, enrollment, labels and webhooks. Improve ReUI frontend data safety and bounded list loading, and add Docker smoke, recovery and resource checks. Document local verification and pending native acceptance.
2026-10-26 13:10:58 +07:00

516 lines
17 KiB
TypeScript

import { act, type ReactNode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import type { PolicyRollout } from '@evofw/shared'
import { WebhooksSheet } from '@/components/webhooks-sheet'
import { AgentLabelsSheet } from '@/components/agents/agent-labels-sheet'
import { PolicyRolloutsSheet } from '@/components/rules/policy-rollouts-sheet'
import { AgentCredentialsSheet } from '@/components/agents/agent-credentials-sheet'
import { ensureAuthConfig, setToken, getToken } from '@/lib/auth'
import '@evofw/ui/globals.css'
if (!import.meta.env.DEV) throw new Error('Development checks only')
;(
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true
const results = document.querySelector<HTMLPreElement>('#results')!
const metrics = document.querySelector<HTMLPreElement>('#metrics')!
const root = createRoot(document.querySelector<HTMLDivElement>('#fixture')!)
const qc = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
})
const originalFetch = window.fetch
const originalToken = getToken()
const checks: string[] = []
const calls: { method: string; path: string; body: any }[] = []
const flush = () => new Promise((resolve) => setTimeout(resolve, 75))
function check(value: unknown, label: string) {
if (!value) throw new Error(label)
checks.push(`PASS ${label}`)
results.textContent = checks.join('\n')
}
async function render(node: ReactNode) {
await act(async () => {
root.render(<QueryClientProvider client={qc}>{node}</QueryClientProvider>)
await flush()
})
await act(async () => {
await flush()
})
}
async function click(text: string) {
await act(async () => {
const button = [
...document.querySelectorAll<HTMLButtonElement>('button'),
].find((item) => item.textContent?.trim() === text && !item.disabled)
if (!button) throw new Error(`Button missing or disabled: ${text}`)
button.click()
await flush()
})
}
function button(text: string) {
return [...document.querySelectorAll<HTMLButtonElement>('button')].find(
(item) => item.textContent?.trim() === text,
)
}
async function input(id: string, value: string) {
await act(async () => {
const field = document.getElementById(id) as HTMLInputElement
Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
)!.set!.call(field, value)
field.dispatchEvent(new Event('input', { bubbles: true }))
await flush()
})
}
async function submit() {
await act(async () => {
document
.querySelector<HTMLFormElement>('[data-slot="sheet-content"] form')!
.requestSubmit()
await flush()
})
}
async function choose(id: string, label: string) {
await act(async () => {
const field = document.getElementById(id)!
field.focus()
field.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }))
await flush()
await flush()
})
await act(async () => {
const option = [
...document.querySelectorAll<HTMLElement>('[role="option"]'),
].find((item) => item.textContent?.trim() === label)
if (!option) throw new Error(`Option missing: ${label}`)
option.click()
await flush()
})
await act(async () => {
document
.getElementById(id)
?.dispatchEvent(
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
)
await flush()
})
}
async function chooseSelect(index: number, label: string) {
await act(async () => {
document
.querySelectorAll<HTMLElement>('[data-slot="select-trigger"]')
[index].click()
await flush()
})
await act(async () => {
const option = [
...document.querySelectorAll<HTMLElement>('[role="option"]'),
].find((item) => item.textContent?.trim() === label)
if (!option) throw new Error(`Select option missing: ${label}`)
option.click()
await flush()
})
}
const writes = (path: string) =>
calls.filter((call) => call.method !== 'GET' && call.path === path)
const fixtureToken = (permissions: string[]) =>
`fixture.${btoa(JSON.stringify({ sub: 'fixture', permissions, apps: ['fw'] }))}.unsigned`
let configured = true,
failHook = true,
failLabels = true,
pendingRotation = false
const endpoint = {
id: 'hook',
name: 'Existing endpoint',
url: 'https://fixture.invalid/hook',
events: ['agent.offline'],
enabled: false,
secret_configured: true,
created_at: '',
updated_at: '',
}
let rollout: PolicyRollout = {
id: 'rollout',
snapshot_id: 'snapshot',
state: 'canary',
version: 9,
created_at: '2026-10-24',
updated_at: '',
targets: [
{
agent_id: 'target',
is_canary: true,
phase: 'candidate',
desired_revision: 8,
desired_hash: 'new',
applied_revision: null,
applied_hash: null,
acknowledged: false,
previous_desired_snapshot_id: 'previous',
},
],
}
window.fetch = async (request, init) => {
const url = String(request),
path = new URL(url, location.origin).pathname,
method = init?.method ?? 'GET'
const body = init?.body ? JSON.parse(String(init.body)) : null
calls.push({ method, path, body })
metrics.textContent = JSON.stringify(
calls.map(({ method, path }) => ({ method, path })),
null,
2,
)
if (path === '/api/auth/config') return Response.json({ required: true })
if (path === '/api/v1/agents')
return Response.json({
items: [
{ id: 'target', name: 'Fixture target', hostname: 'fixture.invalid' },
],
})
if (path === '/api/v1/webhooks') {
if (method === 'POST')
return failHook
? Response.json(
{ error: { message: 'Fixture webhook failure' } },
{ status: 503 },
)
: Response.json(endpoint)
return Response.json({
items: [endpoint],
configured,
limits: { endpoints: 10, pending: 100 },
queue: { pending: 1, dead: 2, dropped: 3 },
})
}
if (path === '/api/v1/webhooks/hook') return Response.json(endpoint)
if (path.endsWith('/deliveries'))
return Response.json({ items: [], total: 0 })
if (path === '/api/v1/agent-labels')
return Response.json({
items: [
{ id: 'label-a', name: 'Fixture label A', color: null, created_at: '' },
{ id: 'label-b', name: 'Fixture label B', color: null, created_at: '' },
],
total: 2,
})
if (path.endsWith('/labels')) {
if (method === 'PUT' && failLabels)
return Response.json(
{ error: { message: 'Fixture label write rejected' } },
{ status: 409 },
)
return Response.json({ agent_id: 'target', label_ids: ['label-a'] })
}
if (path === '/api/v1/policy-sets')
return Response.json({ items: [{ id: 'set', name: 'Fixture set' }] })
if (path.endsWith('/preview-assignment'))
return Response.json({
mode: body.mode,
items: [
{
agent_id: 'target',
generation: 7,
previous_set_ids: [],
set_ids: ['set'],
changed: true,
},
],
total: 1,
})
if (path.endsWith('/assign-policy-sets'))
return Response.json({ items: [], affected_agent_ids: ['target'] })
if (path === '/api/v1/policies/snapshots')
return Response.json({
items: [{ id: 'snapshot', name: 'Fixture snapshot' }],
})
if (path.endsWith('/diff'))
return Response.json({
snapshot_id: 'snapshot',
items: [
{
agent_id: 'target',
expected_generation: 7,
before_hash: 'old',
candidate_hash: 'new',
added: 1,
removed: 0,
examples: [],
truncated: false,
default_action_changed: false,
port_rules_changed: false,
conflict: null,
},
],
})
if (path === '/api/v1/policies/rollouts')
return method === 'POST'
? Response.json(rollout)
: Response.json({ items: [rollout] })
if (path === '/api/v1/policies/rollouts/rollout')
return Response.json(rollout)
if (
path.endsWith('/confirm') ||
path.endsWith('/rollback') ||
path.endsWith('/release')
)
return Response.json({ ok: true })
if (path.endsWith('/token-rotation')) {
if (method === 'POST') {
pendingRotation = true
return Response.json(
{
request_id: '00000000-0000-4000-8000-000000000001',
expires_at: '2099-01-01T00:00:00Z',
},
{ status: 202 },
)
}
return Response.json({
agent_id: 'target',
supported: true,
client_version: '1.0.0',
agent_status: 'approved',
rotation: pendingRotation
? {
request_id: '00000000-0000-4000-8000-000000000001',
expires_at: '2099-01-01T00:00:00Z',
}
: null,
completed_rotation: null,
})
}
if (path === '/api/v1/install-links')
return Response.json({
items: [
{
id: 'install',
slug: 'not-rendered',
client_name: 'Fixture target',
platform: 'linux',
agent_id: 'target',
created_at: '2026-10-24',
expires_at: '2026-10-25',
consumed_at: '2026-10-24',
revoked_at: null,
use_count: 1,
urls: { by_id: 'not-rendered', by_slug: 'not-rendered' },
curl: {
by_id: 'credential-must-not-render',
by_slug: 'credential-must-not-render',
},
},
],
total: 1,
})
throw new Error(`Unexpected fixture request ${method} ${path}`)
}
try {
await ensureAuthConfig()
setToken(
fixtureToken(['fw:agents:write', 'fw:policies:write', 'fw:settings:admin']),
)
await render(<WebhooksSheet open onOpenChange={() => {}} />)
check(
document.body.textContent?.includes('1 ожидают') &&
document.body.textContent.includes('2 исчерпали'),
'webhook health keeps pending/dead/dropped distinct',
)
await input('webhook-name', 'Draft hook')
await input('webhook-url', 'https://fixture.invalid/new')
await input('webhook-secret', 'short')
await submit()
check(
writes('/api/v1/webhooks').length === 0 &&
document
.getElementById('webhook-secret')
?.getAttribute('aria-invalid') === 'true',
'short webhook secret blocked inline before POST',
)
await input('webhook-secret', 'fixture-only-never-delivered-0000000000')
await submit()
check(
writes('/api/v1/webhooks').length === 1 &&
(document.getElementById('webhook-name') as HTMLInputElement).value ===
'Draft hook',
'failed webhook write preserves dirty draft',
)
failHook = false
await submit()
check(
(document.getElementById('webhook-secret') as HTMLInputElement).value ===
'' && writes('/api/v1/webhooks')[1].body.enabled === false,
'successful create clears write-only secret and defaults disabled',
)
await click('Изменить')
await input('webhook-name', 'Renamed hook')
await submit()
check(
writes('/api/v1/webhooks/hook')[0]?.body.name === 'Renamed hook' &&
!('secret' in writes('/api/v1/webhooks/hook')[0].body),
'edit omits blank secret instead of clearing existing secret',
)
configured = false
await act(async () => {
await qc.invalidateQueries({ queryKey: ['webhooks'] })
await flush()
})
check(
button('Добавить webhook')?.disabled &&
document.body.textContent?.includes('Шифрование webhook не настроено'),
'missing encryption key disables writes with explanation',
)
await render(null)
qc.clear()
setToken(fixtureToken(['fw:agents:read']))
const previousCalls = calls.length
await render(<WebhooksSheet key="denied" open onOpenChange={() => {}} />)
check(
calls.length === previousCalls &&
document.body.textContent?.includes('Требуются права администратора'),
'non-admin webhook panel fetches and writes nothing',
)
await render(null)
qc.clear()
setToken(fixtureToken(['fw:agents:write', 'fw:policies:write']))
await render(
<AgentLabelsSheet agentId="target" open onOpenChange={() => {}} />,
)
await choose('agent-label-ids', 'Fixture label B')
await act(async () => {
await qc.invalidateQueries({ queryKey: ['agents', 'target', 'labels'] })
await flush()
})
check(
document.body.textContent?.includes('Fixture label B'),
'agent labels retain dirty selection across refetch',
)
await click('Сохранить метки агента')
check(
writes('/api/v1/agents/target/labels')[0]?.body.label_ids.includes(
'label-b',
) && !button('Сохранить метки агента')?.disabled,
'rejected labels write preserves editable selection',
)
await render(null)
qc.clear()
await render(<AgentLabelsSheet open onOpenChange={() => {}} />)
await choose('group-label-selector', 'Fixture label A')
await choose('group-sets', 'Fixture set')
await click('Предпросмотр назначения')
check(
writes('/api/v1/agent-labels/assign-policy-sets').length === 0 &&
document.body.textContent?.includes('Fixture target'),
'group preview resolves named agents without auto-assignment',
)
await click('Применить показанное назначение')
check(
writes('/api/v1/agent-labels/assign-policy-sets').length === 0,
'group write requires explicit confirmation',
)
await click('Подтвердить')
check(
JSON.stringify(
writes('/api/v1/agent-labels/assign-policy-sets')[0]?.body.expected,
) === JSON.stringify([{ agent_id: 'target', generation: 7 }]),
'group assignment sends exact preview CAS generations',
)
await render(null)
qc.clear()
await render(<PolicyRolloutsSheet open onOpenChange={() => {}} />)
await chooseSelect(0, 'Fixture snapshot')
await choose('rollout-targets', 'Fixture targetfixture.invalid')
check(
button('Запустить canary')?.disabled,
'canary start gated on prior diff',
)
await click('Предпросмотр diff')
await choose('rollout-canaries', 'Fixture targetfixture.invalid')
await click('Запустить canary')
check(
writes('/api/v1/policies/rollouts').length === 0,
'canary publication requires separate confirmation',
)
await click('Подтвердить')
check(
writes('/api/v1/policies/rollouts')[0]?.body.targets[0]
.expected_generation === 7 &&
button('Подтвердить после ACK')?.disabled &&
document.body.textContent?.includes('Ожидается ACK'),
'canary uses diff generation and pending ACK never enables confirmation',
)
rollout = {
...rollout,
targets: [
{
...rollout.targets[0],
acknowledged: true,
applied_revision: 8,
applied_hash: 'new',
},
],
}
await act(async () => {
await qc.invalidateQueries({ queryKey: ['policy-rollouts'] })
await flush()
})
check(
!button('Подтвердить после ACK')?.disabled &&
writes('/api/v1/policies/rollouts/rollout/confirm').length === 0,
'fresh canary ACK enables but does not auto-confirm',
)
await click('Подтвердить после ACK')
await click('Подтвердить')
check(
writes('/api/v1/policies/rollouts/rollout/confirm')[0]?.body
.expected_version === 9,
'rollout transition sends exact displayed version',
)
await click('Снять pin')
check(
document.body.textContent?.includes('живая политика будет проверена') &&
writes('/api/v1/agents/target/policy-pin/release').length === 0,
'pin release warns about validated live-policy resume before write',
)
await click('Отмена')
await render(null)
qc.clear()
await render(
<AgentCredentialsSheet agentId="target" open onOpenChange={() => {}} />,
)
check(
document.body.textContent?.includes('Использована') &&
!document.body.textContent.includes('credential-must-not-render'),
'credential panel displays link lifecycle without installation credentials',
)
await click('Запросить ротацию')
check(
writes('/api/v1/agents/target/token-rotation').length === 0,
'rotation requires explicit request confirmation',
)
await click('Запросить')
check(
pendingRotation &&
document.body.textContent?.includes('Ожидается обмен ключа агентом') &&
button('Запросить ротацию')?.disabled,
'rotation request remains pending until agent exchange',
)
results.dataset.status = 'passed'
} catch (error) {
results.textContent = `${checks.join('\n')}\nFAIL ${error instanceof Error ? error.stack : String(error)}`
results.dataset.status = 'failed'
} finally {
if (results.dataset.status === 'passed') {
await act(async () => root.unmount())
qc.clear()
window.fetch = originalFetch
}
if (originalToken) setToken(originalToken)
else localStorage.removeItem('fw_auth_token')
}