mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 01:20:45 +08:00
Ignore hub alarms before auth-ok, stop reconnecting after auth-fail, serialize sticky delivery with dispose on reset, and sanitize ZIP entry names against path traversal. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.4 KiB
JavaScript
90 lines
3.4 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { test } from 'node:test'
|
|
|
|
import {
|
|
safePathSegment,
|
|
sessionsExportEntries,
|
|
sessionsExportZipFilename,
|
|
} from '../src/netx/session-export.ts'
|
|
|
|
test('safePathSegment strips path separators and keeps filename extensions', () => {
|
|
assert.equal(safePathSegment('../a/b'), 'b')
|
|
assert.equal(safePathSegment('..\\evil.jsonl'), 'evil.jsonl')
|
|
assert.equal(safePathSegment('session.jsonl'), 'session.jsonl')
|
|
assert.equal(safePathSegment('netxops-alarm-abc'), 'netxops-alarm-abc')
|
|
})
|
|
|
|
test('sessionsExportZipFilename stamps host and UTC time', () => {
|
|
const name = sessionsExportZipFilename(new Date('2026-09-06T04:59:00.000Z'), 'edge-01')
|
|
assert.equal(name, 'dsh-sessions-edge-01-20260906-045900.zip')
|
|
})
|
|
|
|
test('sessionsExportEntries writes manifest then artifacts; skips missing', async () => {
|
|
const persistence = {
|
|
supportsRawArtifacts: true,
|
|
list: async () => ([
|
|
{ id: 's1', version: 0, createdAt: 1, agentPreset: 'netxops' },
|
|
{ id: 'missing', version: 0, createdAt: 2 },
|
|
]),
|
|
readRaw: async (id) => {
|
|
if (id === 'missing') return undefined
|
|
return {
|
|
meta: { id, version: 0, createdAt: 1, agentPreset: 'netxops' },
|
|
filename: 'session.jsonl',
|
|
content: '{"type":"header"}\n',
|
|
}
|
|
},
|
|
}
|
|
const ctx = {
|
|
get: (name) => (name === 'sessionPersistence' ? persistence : undefined),
|
|
}
|
|
const entries = []
|
|
for await (const entry of sessionsExportEntries(ctx, undefined)) {
|
|
entries.push(entry)
|
|
}
|
|
assert.equal(entries.length, 2)
|
|
assert.equal(entries[0].path, 'manifest.json')
|
|
const manifest = JSON.parse(entries[0].content)
|
|
assert.equal(manifest.kind, 'dsh-netxops-sessions-export')
|
|
assert.equal(manifest.sessionCountListed, 2)
|
|
assert.equal(manifest.sessionCountIncluded, 1)
|
|
assert.equal(manifest.sessionCountSkipped, 1)
|
|
assert.equal(entries[1].path, 'sessions/s1/session.jsonl')
|
|
assert.match(entries[1].content, /header/)
|
|
})
|
|
|
|
test('writeSessionsExportZip lands under exports/ with a real zip', async () => {
|
|
const { mkdtempSync, readFileSync, rmSync } = await import('node:fs')
|
|
const { tmpdir } = await import('node:os')
|
|
const { join } = await import('node:path')
|
|
const { unzipSync, strFromU8 } = await import('fflate')
|
|
const home = mkdtempSync(join(tmpdir(), 'netxops-export-'))
|
|
const previous = process.env.DSH_HOME
|
|
process.env.DSH_HOME = home
|
|
try {
|
|
const persistence = {
|
|
supportsRawArtifacts: true,
|
|
list: async () => ([{ id: 's1', version: 0, createdAt: 1 }]),
|
|
readRaw: async () => ({
|
|
meta: { id: 's1', version: 0, createdAt: 1 },
|
|
filename: 'session.jsonl',
|
|
content: '{"type":"header"}\n',
|
|
}),
|
|
}
|
|
const ctx = {
|
|
get: (name) => (name === 'sessionPersistence' ? persistence : undefined),
|
|
}
|
|
const { writeSessionsExportZip } = await import('../src/netx/session-export.ts')
|
|
const result = await writeSessionsExportZip(ctx)
|
|
assert.equal(result.sessionCountIncluded, 1)
|
|
assert.ok(result.path.includes(`${join('exports')}`))
|
|
assert.ok(result.bytes > 0)
|
|
const unzipped = unzipSync(readFileSync(result.path))
|
|
assert.ok(unzipped['manifest.json'])
|
|
assert.match(strFromU8(unzipped['sessions/s1/session.jsonl']), /header/)
|
|
} finally {
|
|
if (previous === undefined) delete process.env.DSH_HOME
|
|
else process.env.DSH_HOME = previous
|
|
rmSync(home, { recursive: true, force: true })
|
|
}
|
|
})
|