Files
EvoFirewall/apps/api/src/services/labels.test.ts
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

118 lines
8.0 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest'
import { createMemoryDb, runMigrations, repos } from '@evofw/db'
import { previewLabelAssignmentSchema, applyLabelAssignmentSchema, permissionForRequest } from '@evofw/shared'
import { createAgentLabel, patchAgentLabel, putAgentLabels, previewLabelAssignment, applyLabelAssignment } from './labels.js'
import { createPolicySnapshot, startPolicyRollout } from './policy/rollouts.js'
import { buildApp } from '../app.js'
import { loadConfig } from '../config.js'
const opened: ReturnType<typeof createMemoryDb>[] = []
afterEach(() => { for (const mem of opened.splice(0)) mem.sqlite.close() })
function memory(count = 3) {
const mem = createMemoryDb(); opened.push(mem); runMigrations(mem.sqlite)
for (let index = 0; index < count; index++) repos.insertAgent(mem.db, { id: `a${index}`, name: `Agent ${index}`, tokenHash: `t${index}`, tokenPrefix: `t${index}`, status: 'approved', telemetryVersion: 2, capabilitiesJson: '["policy-ipv4"]' })
for (const id of ['old', 'new', 'third']) repos.insertPolicySet(mem.db, { id, name: id })
return mem
}
describe('labels and materialized group assignment', () => {
it('treats labels as metadata, uses normalized unique names and atomically validates replacement', () => {
const { db } = memory()
const label = createAgentLabel(db, { name: 'Production', color: 'success' })
expect(() => createAgentLabel(db, { name: 'PRODUCTION' })).toThrow('already exists')
putAgentLabels(db, 'a0', [label.id])
patchAgentLabel(db, label.id, { name: 'Production fleet' })
expect(repos.getAgent(db, 'a0')!.policyGeneration).toBe(1)
expect(() => putAgentLabels(db, 'a0', ['missing'])).toThrow('Label not found')
expect(repos.agentLabelIds(db, 'a0')).toEqual([label.id])
repos.deleteAgentLabel(db, label.id)
expect(repos.agentLabelIds(db, 'a0')).toEqual([])
expect(repos.getAgent(db, 'a0')!.policyGeneration).toBe(1)
})
it('preserves existing order in add, performs exact replace and skips identical assignments', () => {
const { db } = memory()
repos.setAgentPolicySets(db, 'a0', ['old', 'third'])
const request = { selector: { agent_ids: ['a0', 'a1'] }, mode: 'add' as const, set_ids: ['third', 'new'] }
const preview = previewLabelAssignment(db, request)
expect(preview.items[0]!.set_ids).toEqual(['old', 'third', 'new'])
const result = applyLabelAssignment(db, { ...request, expected: preview.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) })
expect(result.affected_agent_ids).toEqual(['a0', 'a1'])
const again = previewLabelAssignment(db, request)
const unchanged = applyLabelAssignment(db, { ...request, expected: again.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) })
expect(unchanged.affected_agent_ids).toEqual([])
expect(unchanged.items.map((row) => row.generation)).toEqual(again.items.map((row) => row.generation))
const replace = { selector: { agent_ids: ['a0'] }, mode: 'replace' as const, set_ids: ['new'] }
const replacement = previewLabelAssignment(db, replace)
applyLabelAssignment(db, { ...replace, expected: replacement.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) })
expect(repos.listSetsForAgent(db, 'a0').map((row) => row.setId)).toEqual(['new'])
})
it('requires exact materialized membership and generations; later labels never autoassign', () => {
const { db } = memory()
const label = createAgentLabel(db, { name: 'Canaries' })
putAgentLabels(db, 'a0', [label.id])
const request = { selector: { label_ids: [label.id] }, mode: 'replace' as const, set_ids: ['new'] }
const preview = previewLabelAssignment(db, request)
const expected = preview.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation }))
putAgentLabels(db, 'a1', [label.id])
expect(() => applyLabelAssignment(db, { ...request, expected })).toThrow('membership')
expect(repos.listSetsForAgent(db, 'a0')).toHaveLength(0)
putAgentLabels(db, 'a1', [])
applyLabelAssignment(db, { ...request, expected })
putAgentLabels(db, 'a1', [label.id])
expect(repos.listSetsForAgent(db, 'a1')).toHaveLength(0)
expect(() => applyLabelAssignment(db, { ...request, expected })).toThrow('generation')
})
it('rejects invalid sets and pinned targets before writing any agent', () => {
const { db } = memory()
const request = { selector: { agent_ids: ['a0', 'a1'] }, mode: 'replace' as const, set_ids: ['new'] }
const preview = previewLabelAssignment(db, request)
const expected = preview.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation }))
expect(() => applyLabelAssignment(db, { ...request, set_ids: ['new', 'missing'], expected })).toThrow('Policy set not found')
expect(repos.listSetsForAgent(db, 'a0')).toHaveLength(0)
const snapshot = createPolicySnapshot(db, 'a2', 'Empty')
startPolicyRollout(db, { snapshot_id: snapshot.id, targets: [{ agent_id: 'a1', expected_generation: 1 }], canary_ids: ['a1'] })
expect(() => applyLabelAssignment(db, { ...request, expected })).toThrow('Release')
expect(repos.listSetsForAgent(db, 'a0')).toHaveLength(0)
expect(repos.getAgent(db, 'a0')!.policyGeneration).toBe(1)
})
it('supports exactly 100 selected agents and rejects a 101-agent label group without truncation', () => {
const { db } = memory(101)
const label = createAgentLabel(db, { name: 'Large group' })
for (let index = 0; index < 100; index++) putAgentLabels(db, `a${index}`, [label.id])
const request = { selector: { label_ids: [label.id] }, mode: 'add' as const, set_ids: ['new'] }
const preview = previewLabelAssignment(db, request)
expect(preview.total).toBe(100)
const applied = applyLabelAssignment(db, { ...request, expected: preview.items.map((row) => ({ agent_id: row.agent_id, generation: row.generation })) })
expect(applied.affected_agent_ids).toHaveLength(100)
expect(repos.listSetsForAgent(db, 'a100')).toHaveLength(0)
putAgentLabels(db, 'a100', [label.id])
expect(() => previewLabelAssignment(db, request)).toThrow('100 agents')
expect(repos.listSetsForAgent(db, 'a100')).toHaveLength(0)
expect(previewLabelAssignmentSchema.safeParse({ ...request, selector: { agent_ids: ['a0'], label_ids: [label.id] } }).success).toBe(false)
expect(applyLabelAssignmentSchema.safeParse({ ...request, expected: [{ agent_id: 'a0', generation: 1 }, { agent_id: 'a0', generation: 1 }] }).success).toBe(false)
})
it('provides HTTP label CRUD, AND fleet filtering and permission separation', async () => {
const app = await buildApp({ memory: true, config: { ...loadConfig(), authRequired: false, staticDir: null, logLevel: 'error' } })
try {
await app.ready()
for (const id of ['a', 'b']) repos.insertAgent(app.db, { id, name: id, tokenPrefix: id, tokenHash: id })
const one = await app.inject({ method: 'POST', url: '/api/v1/agent-labels', payload: { name: 'One' } })
const two = await app.inject({ method: 'POST', url: '/api/v1/agent-labels', payload: { name: 'Two' } })
expect(one.statusCode).toBe(201)
expect(one.json().name_key).toBeUndefined()
await app.inject({ method: 'PUT', url: '/api/v1/agents/a/labels', payload: { label_ids: [one.json().id, two.json().id] } })
await app.inject({ method: 'PUT', url: '/api/v1/agents/b/labels', payload: { label_ids: [one.json().id] } })
const filtered = await app.inject({ url: `/api/v1/agents?label_ids=${one.json().id},${two.json().id}` })
expect(filtered.json().items.map((row: { id: string }) => row.id)).toEqual(['a'])
expect((await app.inject({ url: '/api/v1/agents?label_ids=' })).json().items).toHaveLength(2)
expect(permissionForRequest('POST', '/api/v1/agent-labels/preview-assignment')).toBe('fw:policies:read')
expect(permissionForRequest('POST', '/api/v1/agent-labels/assign-policy-sets')).toBe('fw:policies:write')
expect(permissionForRequest('POST', '/api/v1/agent-labels')).toBe('fw:agents:write')
} finally { await app.close() }
})
})