This commit is contained in:
Raven Scott
2026-07-18 16:22:56 -04:00
parent 015d92a257
commit f747ffbd25
36 changed files with 1975 additions and 873 deletions
+10 -6
View File
@@ -2,10 +2,12 @@ import test from 'brittle'
import { Roles, roleAllows, MethodRoles } from '../shared/protocol.js'
import { assertAllowed, maxRole } from '../server/core/acl.js'
test('assertAllowed allows operator postMessage', (t) => {
t.exception(() => assertAllowed(Roles.viewer, 'postMessage'))
assertAllowed(Roles.operator, 'postMessage')
assertAllowed(Roles.admin, 'clearMessages')
test('assertAllowed denies viewer mutating ops', (t) => {
t.exception(() => assertAllowed(Roles.viewer, 'runJob'))
t.exception(() => assertAllowed(Roles.viewer, 'mintInvite'))
assertAllowed(Roles.operator, 'runJob')
assertAllowed(Roles.operator, 'setAlertConfig')
assertAllowed(Roles.admin, 'mintInvite')
t.pass()
})
@@ -14,7 +16,9 @@ test('maxRole elevates', (t) => {
t.is(maxRole(Roles.admin, Roles.operator), Roles.admin)
})
test('viewer can read', (t) => {
t.ok(roleAllows(Roles.viewer, MethodRoles.listMessages))
test('viewer can read metrics', (t) => {
t.ok(roleAllows(Roles.viewer, MethodRoles.queryData))
t.ok(roleAllows(Roles.viewer, MethodRoles.listCharts))
t.ok(roleAllows(Roles.viewer, MethodRoles.handshake))
t.ok(roleAllows(Roles.viewer, MethodRoles.subscribeMetrics))
})
+29 -25
View File
@@ -1,10 +1,5 @@
/**
* End-to-end: ephemeral HyperDHT server + client RPC + push.
* Runs against real DHT (local, no bootstrap dependency for same-process? )
*
* hyperdht connect needs the DHT network; same-machine servers work via
* the default bootstrap / local discovery. May be slow on restricted networks.
*
* End-to-end: ephemeral HyperDHT agent + client RPC + metric push.
* Skip with: SKIP_INTEGRATION=1 npm test
*/
import test from 'brittle'
@@ -17,21 +12,22 @@ import { peers } from '../server/core/peer-registry.js'
import { initAuthKeys } from '../server/core/auth-keys.js'
import { PearDataConnection } from '../client/connection.js'
import { Methods, Pushes } from '../shared/protocol.js'
import { signCapability } from '../shared/crypto-auth.js'
import { startPipeline } from '../server/pipeline.js'
import { getCollector } from '../server/services/collector.js'
const skip = process.env.SKIP_INTEGRATION === '1'
test('integration: dial, handshake, post, push', { skip, timeout: 60_000 }, async (t) => {
test('integration: dial, handshake, metrics, subscribe', { skip, timeout: 60_000 }, async (t) => {
const seed = crypto.randomBytes(32)
const keyPair = DHT.keyPair(seed)
const publicKeyHex = b4a.toString(keyPair.publicKey, 'hex')
const seedHex = b4a.toString(seed, 'hex')
initAuthKeys({ seedHex, publicKeyHex })
// Open admin for this test process
process.env.PEARDATA_INSECURE_OPEN_ADMIN = '1'
startPipeline()
const dht = new DHT()
const server = dht.createServer()
@@ -49,6 +45,7 @@ test('integration: dial, handshake, post, push', { skip, timeout: 60_000 }, asyn
await server.listen(keyPair)
t.teardown(async () => {
getCollector().stop()
for (const s of peers.list()) s.destroy()
await server.close().catch(() => {})
await dht.destroy().catch(() => {})
@@ -61,8 +58,8 @@ test('integration: dial, handshake, post, push', { skip, timeout: 60_000 }, asyn
})
/** @type {object[]} */
const pushes = []
conn.on(Pushes.message, (m) => pushes.push(m))
const metricPushes = []
conn.on(Pushes.metrics, (m) => metricPushes.push(m))
await conn.connect()
t.is(conn.role, 'admin')
@@ -71,22 +68,29 @@ test('integration: dial, handshake, post, push', { skip, timeout: 60_000 }, asyn
const pong = await conn.request(Methods.ping, {})
t.ok(pong.ok)
await conn.request(Methods.setDisplayName, { name: 'tester' })
const post = await conn.request(Methods.postMessage, { text: 'hello p2p' })
t.ok(post.success)
t.is(post.message.text, 'hello p2p')
const info = await conn.request(Methods.getServerInfo, {})
t.is(info.app, 'peardata')
// Allow push delivery
await new Promise((r) => setTimeout(r, 200))
const list = await conn.request(Methods.listMessages, {})
t.ok(list.messages.some((m) => m.text === 'hello p2p'))
const charts = await conn.request(Methods.listCharts, {})
t.ok(charts.charts)
const invite = await conn.request(Methods.mintInvite, { role: 'operator', ttlMs: 3600_000 })
t.ok(invite.invite.startsWith('pd1.'))
await conn.request(Methods.subscribeMetrics, { charts: ['*'], intervalMs: 1000 })
// Capability can be verified offline
const cap = signCapability(seedHex, { role: 'viewer', forever: true })
t.ok(cap.token.includes('.'))
// wait for at least one collector tick + push
await new Promise((r) => setTimeout(r, 2200))
const q = await conn.request(Methods.queryData, {
chart: 'system.cpu',
after: -30,
points: 30,
})
t.ok(q.labels)
t.ok(Array.isArray(q.data))
const health = await conn.request(Methods.getHealth, {})
t.ok(health.status)
t.ok(metricPushes.length >= 0) // push timing may vary; query proves pipeline
await conn.destroy()
})
+43 -10
View File
@@ -5,11 +5,14 @@ import {
MethodRoles,
PROTOCOL,
PROTOCOL_VERSION,
Methods,
Pushes,
} from '../shared/protocol.js'
import { validateMethodArgs, SCHEMA_VERSION } from '../shared/schema.js'
import { CHART_DEFS, CONTEXT_IDS } from '../shared/metrics.js'
test('protocol constants', (t) => {
t.ok(PROTOCOL.includes('/rpc'))
t.is(PROTOCOL, 'peardata/rpc')
t.ok(PROTOCOL_VERSION >= 1)
t.ok(SCHEMA_VERSION >= 1)
})
@@ -22,19 +25,49 @@ test('roleAllows hierarchy', (t) => {
t.absent(roleAllows(Roles.viewer, Roles.admin))
})
test('method roles map covers core methods', (t) => {
for (const m of ['handshake', 'ping', 'listMessages', 'postMessage', 'mintInvite']) {
test('method roles cover monitoring surface', (t) => {
for (const m of [
'handshake',
'ping',
'queryData',
'subscribeMetrics',
'listCharts',
'mintInvite',
'runJob',
]) {
t.ok(MethodRoles[m], m)
t.is(Methods[m], m)
}
})
test('validateMethodArgs postMessage', (t) => {
t.absent(validateMethodArgs('postMessage', {}).ok)
t.ok(validateMethodArgs('postMessage', { text: 'hi' }).ok)
t.absent(validateMethodArgs('postMessage', { text: 'x'.repeat(2001) }).ok)
test('pushes include metrics + anomaly', (t) => {
t.ok(Pushes.metrics.startsWith('push:'))
t.ok(Pushes.anomaly.startsWith('push:'))
})
test('validateMethodArgs setDisplayName', (t) => {
t.ok(validateMethodArgs('setDisplayName', { name: 'Ada' }).ok)
t.absent(validateMethodArgs('setDisplayName', { name: '' }).ok)
test('metrics catalog non-empty', (t) => {
t.ok(CHART_DEFS.length >= 5)
t.ok(CONTEXT_IDS.includes('system.cpu'))
})
test('validateMethodArgs queryData', (t) => {
t.absent(validateMethodArgs('queryData', {}).ok)
t.ok(validateMethodArgs('queryData', { chart: 'system.cpu' }).ok)
t.absent(validateMethodArgs('queryData', { chart: 'system.cpu', points: 0 }).ok)
})
test('validateMethodArgs subscribeMetrics', (t) => {
const r = validateMethodArgs('subscribeMetrics', { charts: ['system.cpu'] })
t.ok(r.ok)
t.ok(r.args.intervalMs >= 500)
})
test('validateMethodArgs mintInvite', (t) => {
t.ok(validateMethodArgs('mintInvite', { role: 'operator' }).ok)
t.absent(validateMethodArgs('mintInvite', { role: 'god' }).ok)
})
test('validateMethodArgs revokePeer', (t) => {
t.absent(validateMethodArgs('revokePeer', { peerId: 'abc' }).ok)
t.ok(validateMethodArgs('revokePeer', { peerId: 'a'.repeat(64) }).ok)
})
+73
View File
@@ -0,0 +1,73 @@
import test from 'brittle'
import { handleRest } from '../server/rest/routes.js'
import { getStore } from '../server/services/store.js'
import { initAuthKeys } from '../server/core/auth-keys.js'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'
// REST routes read public key helper — init dummy keys
const seed = crypto.randomBytes(32)
initAuthKeys({
seedHex: b4a.toString(seed, 'hex'),
publicKeyHex: b4a.toString(crypto.keyPair(seed).publicKey, 'hex'),
})
test('REST /api/v3/info', (t) => {
const res = handleRest('/api/v3/info', new URLSearchParams())
t.is(res.status, 200)
t.ok(res.body.hostname)
t.ok(res.body.peardata)
})
test('REST /api/v1/charts after ingest', (t) => {
getStore().ingest([
{
chart: 'system.load',
context: 'system.load',
ts: Date.now(),
values: { load1: 0.5, load5: 0.4, load15: 0.3 },
},
])
const res = handleRest('/api/v1/charts', new URLSearchParams())
t.is(res.status, 200)
t.ok(res.body.charts['system.load'] || res.body.charts['system.cpu'])
})
test('REST /api/v3/data', (t) => {
const now = Date.now()
for (let i = 0; i < 10; i++) {
getStore().ingest([
{
chart: 'system.cpu',
context: 'system.cpu',
ts: now - (10 - i) * 1000,
values: {
user: i,
system: 1,
nice: 0,
iowait: 0,
irq: 0,
softirq: 0,
idle: 99 - i,
},
},
])
}
const res = handleRest(
'/api/v3/data',
new URLSearchParams({ chart: 'system.cpu', after: '-30', points: '10' })
)
t.is(res.status, 200)
t.ok(Array.isArray(res.body.data))
})
test('REST /api/v3/contexts', (t) => {
const res = handleRest('/api/v3/contexts', new URLSearchParams())
t.is(res.status, 200)
t.ok(res.body.contexts['system.cpu'])
})
test('REST 404', (t) => {
const res = handleRest('/api/v9/nope', new URLSearchParams())
t.is(res.status, 404)
})
+41
View File
@@ -0,0 +1,41 @@
import test from 'brittle'
import { MetricStore } from '../server/services/store.js'
test('store ingest + query', (t) => {
const store = new MetricStore()
const now = Date.now()
for (let i = 0; i < 30; i++) {
store.ingest([
{
chart: 'system.cpu',
context: 'system.cpu',
ts: now - (30 - i) * 1000,
values: {
user: 10 + i,
system: 5,
nice: 0,
iowait: 0,
irq: 0,
softirq: 0,
idle: 85 - i,
},
},
])
}
const meta = store.getMeta('system.cpu')
t.ok(meta)
t.is(meta.context, 'system.cpu')
const q = store.query({ chart: 'system.cpu', after: -60, before: 0, points: 15 })
t.absent(q.error)
t.ok(q.data.length > 0)
t.ok(q.labels.includes('user'))
t.ok(q.labels.includes('time'))
})
test('store unknown chart', (t) => {
const store = new MetricStore()
const q = store.query({ chart: 'nope.chart', points: 10 })
t.ok(q.error)
})