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
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.
122 lines
7.8 KiB
TypeScript
122 lines
7.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { createMemoryDb, runMigrations, repos } from '@evofw/db'
|
|
import { buildApp } from '../../app.js'
|
|
import { loadConfig } from '../../config.js'
|
|
import { hashToken } from '../../plugins/auth.js'
|
|
import { evaluateAgentPolicy } from './evaluate.js'
|
|
import { policyCacheFor } from './cache.js'
|
|
import { refreshAllHostnameRules } from './resolve-hostname.js'
|
|
|
|
const dns = vi.hoisted(() => ({ resolve4: vi.fn(), resolve6: vi.fn(), cancel: vi.fn() }))
|
|
vi.mock('node:dns/promises', async (original) => ({ ...await original<typeof import('node:dns/promises')>(), Resolver: class { resolve4 = dns.resolve4; resolve6 = dns.resolve6; cancel = dns.cancel } }))
|
|
const opened: ReturnType<typeof createMemoryDb>[] = []
|
|
const apps: Awaited<ReturnType<typeof buildApp>>[] = []
|
|
afterEach(async () => { for (const app of apps.splice(0)) await app.close(); for (const mem of opened.splice(0)) mem.sqlite.close(); vi.resetAllMocks() })
|
|
function memory() {
|
|
const mem = createMemoryDb(); opened.push(mem); runMigrations(mem.sqlite)
|
|
repos.insertAgent(mem.db, { id: 'a', name: 'a', tokenPrefix: 'a', tokenHash: 'a', status: 'approved' })
|
|
return mem
|
|
}
|
|
function hostnameFixture() {
|
|
const mem = memory(); repos.ensureSharedSetAssigned(mem.db, 'a')
|
|
for (const [id, hostname, cidr, priority] of [['h1', 'first.test', '192.0.2.1/32', 10], ['h2', 'second.test', '192.0.2.2/32', 20]] as const) {
|
|
repos.insertPolicyRule(mem.db, { id, setId: 'set-shared-default', hostname, action: 'deny', priority })
|
|
repos.replaceResolvedForRule(mem.db, id, [cidr])
|
|
}
|
|
dns.resolve6.mockResolvedValue([])
|
|
return mem
|
|
}
|
|
|
|
describe('cache assignment and DNS publication invalidation', () => {
|
|
it('refreshes cached preview set names after a rename without bumping again for an identical name', async () => {
|
|
const app = await buildApp({ memory: true, config: { ...loadConfig(), authRequired: false, staticDir: null, logLevel: 'error' } }); apps.push(app); await app.ready()
|
|
repos.insertAgent(app.db, { id: 'a', name: 'a', tokenPrefix: 'a', tokenHash: 'a', status: 'approved' })
|
|
repos.ensureSharedSetAssigned(app.db, 'a')
|
|
repos.insertPolicyRule(app.db, { id: 'deny', setId: 'set-shared-default', action: 'deny', cidr: '10.0.0.0/8', priority: 10 })
|
|
const before = (await app.inject({ url: '/api/v1/agents/a/preview' })).json()
|
|
const renamed = await app.inject({ method: 'PATCH', url: '/api/v1/policy-sets/set-shared-default', payload: { name: 'Renamed source set' } })
|
|
expect(renamed.statusCode).toBe(200)
|
|
const after = (await app.inject({ url: '/api/v1/agents/a/preview' })).json()
|
|
expect(after.chain[0].set_name).toBe('Renamed source set')
|
|
expect(after.generation).toBe(before.generation + 1)
|
|
expect(after.hash).toBe(before.hash)
|
|
await app.inject({ method: 'PATCH', url: '/api/v1/policy-sets/set-shared-default', payload: { name: 'Renamed source set' } })
|
|
expect((await app.inject({ url: '/api/v1/agents/a/preview' })).json().generation).toBe(after.generation)
|
|
})
|
|
|
|
it('invalidates a preview cached before single and bulk approval, with exactly one insertion bump and idempotent retries', async () => {
|
|
const app = await buildApp({ memory: true, config: { ...loadConfig(), authRequired: false, staticDir: null, logLevel: 'error' } }); apps.push(app); await app.ready()
|
|
repos.insertPolicyRule(app.db, { id: 'deny', setId: 'set-shared-default', action: 'deny', cidr: '10.0.0.0/8', priority: 10 })
|
|
for (const id of ['single', 'bulk']) {
|
|
repos.insertAgent(app.db, { id, name: id, tokenPrefix: id, tokenHash: hashToken(`token-${id}`), status: 'pending' })
|
|
const preview = await app.inject({ url: `/api/v1/agents/${id}/preview` })
|
|
expect(preview.statusCode).toBe(200)
|
|
expect(preview.json().summary.cidrs_deny).toBe(0)
|
|
expect(preview.json().generation).toBe(1)
|
|
}
|
|
const single = await app.inject({ method: 'POST', url: '/api/v1/agents/single/approve' })
|
|
expect(single.statusCode).toBe(200)
|
|
expect(single.json().policy_generation).toBe(2)
|
|
const bulk = await app.inject({ method: 'POST', url: '/api/v1/agents/approve-bulk', payload: { agent_ids: ['bulk'] } })
|
|
expect(bulk.statusCode).toBe(200)
|
|
expect(bulk.json().items[0].policy_generation).toBe(2)
|
|
for (const id of ['single', 'bulk']) {
|
|
const policy = await app.inject({ url: '/v1/agent/policy', headers: { authorization: `Bearer token-${id}` } })
|
|
expect(policy.statusCode).toBe(200)
|
|
expect(policy.json()).toMatchObject({ generation: 2, deny_cidrs: ['10.0.0.0/8'] })
|
|
expect(repos.ensureSharedSetAssigned(app.db, id)).toBe(false)
|
|
expect(repos.getAgent(app.db, id)!.policyGeneration).toBe(2)
|
|
}
|
|
expect((await app.inject({ method: 'POST', url: '/api/v1/agents/single/approve' })).json().policy_generation).toBe(2)
|
|
expect((await app.inject({ method: 'POST', url: '/api/v1/agents/approve-bulk', payload: { agent_ids: ['bulk'] } })).json().items).toEqual([])
|
|
expect(repos.getAgent(app.db, 'bulk')!.policyGeneration).toBe(2)
|
|
})
|
|
|
|
it('rolls back assignment insertion when the generation bump fails, and existing assignments never bump again', () => {
|
|
const mem = memory()
|
|
mem.sqlite.exec("CREATE TRIGGER reject_generation BEFORE UPDATE OF policy_generation ON agents BEGIN SELECT RAISE(ABORT,'test generation failure'); END")
|
|
expect(() => repos.ensureSharedSetAssigned(mem.db, 'a')).toThrow(/generation failure/)
|
|
expect(repos.listSetsForAgent(mem.db, 'a')).toEqual([])
|
|
expect(repos.getAgent(mem.db, 'a')!.policyGeneration).toBe(1)
|
|
mem.sqlite.exec('DROP TRIGGER reject_generation')
|
|
expect(repos.ensureSharedSetAssigned(mem.db, 'a')).toBe(true)
|
|
expect(repos.ensureSharedSetAssigned(mem.db, 'a')).toBe(false)
|
|
expect(repos.getAgent(mem.db, 'a')!.policyGeneration).toBe(2)
|
|
})
|
|
|
|
it('keeps warm and cold previews on the old snapshot while another DNS lookup is pending, then publishes once atomically', async () => {
|
|
const mem = hostnameFixture()
|
|
const before = evaluateAgentPolicy(mem.db, 'a')
|
|
let release!: (value: string[]) => void
|
|
const second = new Promise<string[]>((resolve) => { release = resolve })
|
|
dns.resolve4.mockImplementation((host) => host === 'second.test' ? second : Promise.resolve(['203.0.113.1']))
|
|
const refresh = refreshAllHostnameRules(mem.db)
|
|
await vi.waitFor(() => expect(dns.resolve4).toHaveBeenCalledWith('second.test'))
|
|
expect(repos.listResolvedForRule(mem.db, 'h1').map((row) => row.cidr)).toEqual(['192.0.2.1/32'])
|
|
expect(evaluateAgentPolicy(mem.db, 'a')).toBe(before)
|
|
policyCacheFor(mem.db).get('a', 'force-cold-preview', Date.now())
|
|
expect(evaluateAgentPolicy(mem.db, 'a').hash).toBe(before.hash)
|
|
release(['203.0.113.2']); await refresh
|
|
const after = evaluateAgentPolicy(mem.db, 'a')
|
|
expect(after.denyCidrs).toEqual(['203.0.113.1/32', '203.0.113.2/32'])
|
|
expect(after.generation).toBe(before.generation + 1)
|
|
await refreshAllHostnameRules(mem.db)
|
|
expect(evaluateAgentPolicy(mem.db, 'a')).toBe(after)
|
|
})
|
|
|
|
it('preserves failed lookup snapshots and never overwrites a newer publication made while DNS was pending', async () => {
|
|
const mem = hostnameFixture()
|
|
let reject!: (reason: Error) => void
|
|
const second = new Promise<string[]>((_, fail) => { reject = fail })
|
|
dns.resolve4.mockImplementation((host) => host === 'second.test' ? second : Promise.resolve(['203.0.113.1']))
|
|
const refresh = refreshAllHostnameRules(mem.db)
|
|
await vi.waitFor(() => expect(dns.resolve4).toHaveBeenCalledWith('second.test'))
|
|
repos.replaceResolvedForRule(mem.db, 'h1', ['198.51.100.1/32'])
|
|
repos.bumpAgentsForSet(mem.db, 'set-shared-default')
|
|
const newer = evaluateAgentPolicy(mem.db, 'a')
|
|
reject(Object.assign(new Error('timeout'), { code: 'ETIMEOUT' })); await refresh
|
|
expect(evaluateAgentPolicy(mem.db, 'a')).toBe(newer)
|
|
expect(newer.denyCidrs).toEqual(['192.0.2.2/32', '198.51.100.1/32'])
|
|
})
|
|
})
|