chore(ci): request-id, RBAC stats, web-тесты и гигиена CI
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
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
- 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
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { isAgentStale, computeFleetCounts } from './agents-fleet-kpis'
|
||||
import type { Agent } from '@evofw/shared'
|
||||
|
||||
function agent(patch: Partial<Agent>): Agent {
|
||||
return {
|
||||
id: 'a1',
|
||||
name: 'a',
|
||||
hostname: null,
|
||||
platform: 'linux',
|
||||
token_prefix: 'x',
|
||||
status: 'approved',
|
||||
default_action: 'accept',
|
||||
policy_mode: 'blacklist',
|
||||
policy_generation: 1,
|
||||
last_seen_at: null,
|
||||
last_seen_ip: null,
|
||||
last_apply_at: null,
|
||||
last_apply_status: null,
|
||||
last_apply_error: null,
|
||||
last_apply_prefix_count: null,
|
||||
last_apply_packets_dropped: null,
|
||||
last_apply_packets_accepted: null,
|
||||
total_packets_dropped: 0,
|
||||
total_packets_accepted: 0,
|
||||
last_apply_kernel_method: null,
|
||||
client_version: null,
|
||||
created_at: '2026-01-01T00:00:00Z',
|
||||
approved_at: null,
|
||||
revoked_at: null,
|
||||
install_curl: null,
|
||||
install_link_id: null,
|
||||
...patch,
|
||||
} as Agent
|
||||
}
|
||||
|
||||
describe('agents-fleet-kpis', () => {
|
||||
it('stale = approved + unseen or seen >24h ago; never for pending', () => {
|
||||
const now = Date.parse('2026-09-20T12:00:00Z')
|
||||
expect(isAgentStale(agent({ last_seen_at: null }), now)).toBe(true)
|
||||
expect(
|
||||
isAgentStale(agent({ last_seen_at: '2026-09-20T11:00:00Z' }), now),
|
||||
).toBe(false)
|
||||
expect(
|
||||
isAgentStale(agent({ last_seen_at: '2026-09-19T11:00:00Z' }), now),
|
||||
).toBe(true)
|
||||
expect(
|
||||
isAgentStale(
|
||||
agent({ status: 'pending', last_seen_at: null }),
|
||||
now,
|
||||
),
|
||||
).toBe(false)
|
||||
expect(
|
||||
isAgentStale(agent({ last_seen_at: 'garbage' }), now),
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('computeFleetCounts tallies statuses, stale and apply errors', () => {
|
||||
const now = Date.now()
|
||||
const counts = computeFleetCounts([
|
||||
agent({ id: '1', status: 'pending' }),
|
||||
agent({ id: '2', status: 'invited' }),
|
||||
agent({ id: '3', status: 'revoked' }),
|
||||
agent({ id: '4', last_seen_at: new Date(now).toISOString() }),
|
||||
agent({
|
||||
id: '5',
|
||||
last_seen_at: new Date(now - 25 * 3600_000).toISOString(),
|
||||
}),
|
||||
agent({
|
||||
id: '6',
|
||||
last_seen_at: new Date(now).toISOString(),
|
||||
last_apply_error: 'boom',
|
||||
}),
|
||||
])
|
||||
expect(counts.pending).toBe(1)
|
||||
expect(counts.invited).toBe(1)
|
||||
expect(counts.revoked).toBe(1)
|
||||
expect(counts.approved).toBe(3)
|
||||
expect(counts.stale).toBe(1)
|
||||
expect(counts.applyErrors).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
getActiveFilters,
|
||||
applyFiltersToData,
|
||||
} from '@/components/reui-kit/filter-utils'
|
||||
|
||||
type Row = { name: string; status: string }
|
||||
|
||||
const rows: Row[] = [
|
||||
{ name: 'web-01', status: 'approved' },
|
||||
{ name: 'db-01', status: 'pending' },
|
||||
{ name: 'mt-01', status: 'revoked' },
|
||||
]
|
||||
|
||||
const field = (item: Row, f: string) =>
|
||||
f === 'name' ? item.name : f === 'status' ? item.status : undefined
|
||||
|
||||
describe('filter-utils', () => {
|
||||
it('getActiveFilters drops empty filters', () => {
|
||||
const active = getActiveFilters([
|
||||
{ id: '1', field: 'status', operator: 'is', values: ['approved'] },
|
||||
{ id: '2', field: 'name', operator: 'contains', values: [''] },
|
||||
{ id: '3', field: 'x', operator: 'is', values: [] },
|
||||
{ id: '4', field: 'x', operator: 'is', values: [null, undefined] },
|
||||
])
|
||||
expect(active.map((f) => f.id)).toEqual(['1'])
|
||||
})
|
||||
|
||||
it('applies is / is_not / is_any_of operators', () => {
|
||||
expect(
|
||||
applyFiltersToData(
|
||||
rows,
|
||||
[{ id: '1', field: 'status', operator: 'is', values: ['pending'] }],
|
||||
field,
|
||||
),
|
||||
).toEqual([rows[1]])
|
||||
|
||||
expect(
|
||||
applyFiltersToData(
|
||||
rows,
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
field: 'status',
|
||||
operator: 'is_any_of',
|
||||
values: ['pending', 'revoked'],
|
||||
},
|
||||
],
|
||||
field,
|
||||
),
|
||||
).toEqual([rows[1], rows[2]])
|
||||
|
||||
expect(
|
||||
applyFiltersToData(
|
||||
rows,
|
||||
[
|
||||
{
|
||||
id: '1',
|
||||
field: 'status',
|
||||
operator: 'is_not',
|
||||
values: ['revoked'],
|
||||
},
|
||||
],
|
||||
field,
|
||||
),
|
||||
).toEqual([rows[0], rows[1]])
|
||||
})
|
||||
|
||||
it('combines multiple filters with AND', () => {
|
||||
const out = applyFiltersToData(
|
||||
rows,
|
||||
[
|
||||
{ id: '1', field: 'status', operator: 'is', values: ['approved'] },
|
||||
{ id: '2', field: 'name', operator: 'contains', values: ['web'] },
|
||||
],
|
||||
field,
|
||||
)
|
||||
expect(out).toEqual([rows[0]])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseClaims } from './auth'
|
||||
|
||||
function makeToken(payload: object): string {
|
||||
const b64 = (obj: object) =>
|
||||
btoa(JSON.stringify(obj))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '')
|
||||
return `${b64({ alg: 'none' })}.${b64(payload)}.sig`
|
||||
}
|
||||
|
||||
describe('parseClaims', () => {
|
||||
it('parses a portal JWT payload', () => {
|
||||
const claims = parseClaims(
|
||||
makeToken({
|
||||
sub: 'u1',
|
||||
email: '[email protected]',
|
||||
name: 'Admin',
|
||||
apps: ['fw'],
|
||||
permissions: ['fw:agents:write'],
|
||||
is_admin: true,
|
||||
exp: 1893456000,
|
||||
}),
|
||||
)
|
||||
expect(claims?.sub).toBe('u1')
|
||||
expect(claims?.apps).toEqual(['fw'])
|
||||
expect(claims?.is_admin).toBe(true)
|
||||
})
|
||||
|
||||
it('returns null for garbage / wrong shapes', () => {
|
||||
expect(parseClaims('not-a-jwt')).toBeNull()
|
||||
expect(parseClaims('a.b')).toBeNull()
|
||||
expect(parseClaims('%%%')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { NAV_ITEMS, NAV_SECTIONS, navLabel, navParentForDetail } from './nav'
|
||||
|
||||
describe('nav config', () => {
|
||||
it('every nav item has label, icon, keywords and a known section', () => {
|
||||
const sectionIds = new Set(NAV_SECTIONS.map((s) => s.id))
|
||||
for (const item of NAV_ITEMS) {
|
||||
expect(item.label.length).toBeGreaterThan(0)
|
||||
expect(item.keywords.length).toBeGreaterThan(0)
|
||||
expect(sectionIds.has(item.section)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('navLabel resolves known routes', () => {
|
||||
expect(navLabel('/')).toBe('Панель управления')
|
||||
expect(navLabel('/agents')).toBe('Агенты')
|
||||
expect(navLabel('/nope')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('navParentForDetail maps detail routes to parents', () => {
|
||||
expect(navParentForDetail('/agents/abc')).toBe('/agents')
|
||||
expect(navParentForDetail('/lists/abc')).toBe('/lists')
|
||||
expect(navParentForDetail('/rules/set-1')).toBe('/rules')
|
||||
expect(navParentForDetail('/settings')).toBeUndefined()
|
||||
expect(navParentForDetail('/agents/abc/preview')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user