Updates
This commit is contained in:
+10
-6
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
Reference in New Issue
Block a user