mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-11 11:10:21 +00:00
644 lines
22 KiB
TypeScript
644 lines
22 KiB
TypeScript
import { createHash, generateKeyPairSync, sign } from 'crypto'
|
|
import { createServer, type Server } from 'http'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { LanDeviceInfo } from '../../packages/server/src/services/lan-discovery'
|
|
|
|
const keyPair = generateKeyPairSync('ed25519', {
|
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
})
|
|
const deviceId = `hwui_${createHash('sha256').update(keyPair.publicKey).digest('base64url').slice(0, 32)}`
|
|
|
|
const device: LanDeviceInfo = {
|
|
id: deviceId,
|
|
device_id: deviceId,
|
|
device_public_key: keyPair.publicKey,
|
|
computer_name: 'paired-device',
|
|
endpoint_kind: 'web',
|
|
ip: '192.168.1.20',
|
|
http_port: 8648,
|
|
url: 'http://192.168.1.20:8648',
|
|
os: {
|
|
type: 'Linux',
|
|
platform: 'linux',
|
|
release: '1',
|
|
arch: 'x64',
|
|
},
|
|
hermes_agent_version: 'v1',
|
|
hermes_web_ui_version: '1',
|
|
response_ms: 12,
|
|
last_seen_at: new Date().toISOString(),
|
|
}
|
|
|
|
describe('devices controller', () => {
|
|
let db: any = null
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
const { DatabaseSync } = await import('node:sqlite')
|
|
db = new DatabaseSync(':memory:')
|
|
vi.doMock('../../packages/server/src/db/index', () => ({
|
|
getDb: () => db,
|
|
getStoragePath: () => ':memory:',
|
|
}))
|
|
const { initAllHermesTables } = await import('../../packages/server/src/db/hermes/schemas')
|
|
initAllHermesTables()
|
|
})
|
|
|
|
afterEach(() => {
|
|
db?.close()
|
|
db = null
|
|
vi.unstubAllGlobals()
|
|
vi.doUnmock('../../packages/server/src/db/index')
|
|
vi.doUnmock('../../packages/server/src/services/login-limiter')
|
|
vi.resetModules()
|
|
})
|
|
|
|
it('returns the inbound pairing status for a signed device status request', async () => {
|
|
const { requestInboundDeviceLink, updateInboundStatus } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
requestInboundDeviceLink(device)
|
|
updateInboundStatus(device.id, 'approved')
|
|
|
|
const timestamp = Date.now()
|
|
const nonce = 'status-nonce-1'
|
|
const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url')
|
|
const ctx: any = {
|
|
request: {
|
|
body: {
|
|
device_id: device.id,
|
|
device_public_key: device.device_public_key,
|
|
timestamp,
|
|
nonce,
|
|
signature,
|
|
},
|
|
},
|
|
}
|
|
|
|
const { requestDeviceLinkStatusController } = await import('../../packages/server/src/controllers/devices')
|
|
await requestDeviceLinkStatusController(ctx)
|
|
|
|
expect(ctx.status).toBeUndefined()
|
|
expect(ctx.body).toEqual({ status: 'approved' })
|
|
})
|
|
|
|
it('keeps LAN pairing requests code-free and derives the device URL from the request source', async () => {
|
|
const timestamp = Date.now()
|
|
const nonce = 'lan-request-nonce-1'
|
|
const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url')
|
|
const ctx: any = {
|
|
ip: '192.168.1.99',
|
|
request: {
|
|
ip: '192.168.1.99',
|
|
body: {
|
|
...device,
|
|
http_port: 8648,
|
|
url: 'javascript:alert(1)',
|
|
timestamp,
|
|
nonce,
|
|
signature,
|
|
},
|
|
},
|
|
}
|
|
|
|
const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices')
|
|
await requestDeviceLinkController(ctx)
|
|
|
|
expect(ctx.status).toBeUndefined()
|
|
expect(ctx.body).toEqual({ status: 'pending' })
|
|
expect(getDeviceRelation(device.id)?.url).toBe('http://192.168.1.99:8648')
|
|
})
|
|
|
|
it('requires the startup pairing code before public requests enter the approval list', async () => {
|
|
const timestamp = Date.now()
|
|
const nonce = 'public-request-nonce-1'
|
|
const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url')
|
|
const ctx: any = {
|
|
ip: '8.8.8.8',
|
|
request: {
|
|
ip: '8.8.8.8',
|
|
body: {
|
|
...device,
|
|
http_port: 8648,
|
|
timestamp,
|
|
nonce,
|
|
signature,
|
|
},
|
|
},
|
|
}
|
|
|
|
const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices')
|
|
await requestDeviceLinkController(ctx)
|
|
|
|
expect(ctx.status).toBe(403)
|
|
expect(ctx.body).toEqual({ error: 'Invalid pairing code' })
|
|
expect(getDeviceRelation(device.id)).toBeNull()
|
|
})
|
|
|
|
it('does not trust forged forwarded private IPs for public pairing requests', async () => {
|
|
const recordPairingFailure = vi.fn()
|
|
vi.doMock('../../packages/server/src/services/login-limiter', () => ({
|
|
checkPairing: (ip: string) => {
|
|
expect(ip).toBe('8.8.8.8')
|
|
return { allowed: true }
|
|
},
|
|
recordPairingFailure,
|
|
}))
|
|
|
|
const timestamp = Date.now()
|
|
const nonce = 'forged-forwarded-private-request-nonce-1'
|
|
const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url')
|
|
const ctx: any = {
|
|
ip: '8.8.8.8',
|
|
get: (name: string) => name.toLowerCase() === 'x-forwarded-for' ? '192.168.1.44' : '',
|
|
request: {
|
|
ip: '8.8.8.8',
|
|
body: {
|
|
...device,
|
|
http_port: 8648,
|
|
timestamp,
|
|
nonce,
|
|
signature,
|
|
},
|
|
},
|
|
}
|
|
|
|
const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices')
|
|
await requestDeviceLinkController(ctx)
|
|
|
|
expect(ctx.status).toBe(403)
|
|
expect(ctx.body).toEqual({ error: 'Invalid pairing code' })
|
|
expect(recordPairingFailure).toHaveBeenCalledWith('8.8.8.8')
|
|
expect(getDeviceRelation(device.id)).toBeNull()
|
|
})
|
|
|
|
it('requires the startup pairing code for public clients forwarded through loopback proxies', async () => {
|
|
const timestamp = Date.now()
|
|
const nonce = 'forwarded-public-request-nonce-1'
|
|
const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url')
|
|
const ctx: any = {
|
|
ip: '127.0.0.1',
|
|
get: (name: string) => name.toLowerCase() === 'x-forwarded-for' ? '203.0.113.10' : '',
|
|
request: {
|
|
ip: '127.0.0.1',
|
|
body: {
|
|
...device,
|
|
http_port: 8648,
|
|
timestamp,
|
|
nonce,
|
|
signature,
|
|
},
|
|
},
|
|
}
|
|
|
|
const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices')
|
|
await requestDeviceLinkController(ctx)
|
|
|
|
expect(ctx.status).toBe(403)
|
|
expect(ctx.body).toEqual({ error: 'Invalid pairing code' })
|
|
expect(getDeviceRelation(device.id)).toBeNull()
|
|
})
|
|
|
|
it('stores public pairing requests as pending when the startup pairing code is valid', async () => {
|
|
vi.doMock('../../packages/server/src/services/device-pairing-code', () => ({
|
|
getDevicePairingCode: () => 'pair-secret',
|
|
verifyDevicePairingCode: (value: unknown) => value === 'pair-secret',
|
|
}))
|
|
const timestamp = Date.now()
|
|
const nonce = 'public-request-nonce-2'
|
|
const signature = sign(null, Buffer.from(`${device.id}.${nonce}.${timestamp}`), keyPair.privateKey).toString('base64url')
|
|
const ctx: any = {
|
|
ip: '8.8.4.4',
|
|
request: {
|
|
ip: '8.8.4.4',
|
|
body: {
|
|
...device,
|
|
http_port: 8648,
|
|
pairing_code: 'pair-secret',
|
|
timestamp,
|
|
nonce,
|
|
signature,
|
|
},
|
|
},
|
|
}
|
|
|
|
const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
const { requestDeviceLinkController } = await import('../../packages/server/src/controllers/devices')
|
|
await requestDeviceLinkController(ctx)
|
|
|
|
expect(ctx.status).toBeUndefined()
|
|
expect(ctx.body).toEqual({ status: 'pending' })
|
|
expect(getDeviceRelation(device.id)?.inbound_status).toBe('pending')
|
|
expect(getDeviceRelation(device.id)?.url).toBe('http://8.8.4.4:8648')
|
|
})
|
|
|
|
it('prefers public request hosts when building copyable pairing links', async () => {
|
|
vi.doMock('../../packages/server/src/services/device-pairing-code', () => ({
|
|
getDevicePairingCode: () => 'pair-secret',
|
|
verifyDevicePairingCode: () => false,
|
|
}))
|
|
|
|
const { getDevicePairingLink } = await import('../../packages/server/src/controllers/devices')
|
|
const ctx: any = {
|
|
protocol: 'http',
|
|
host: 'studio.example.com',
|
|
get: (name: string) => name.toLowerCase() === 'x-forwarded-proto' ? 'https' : '',
|
|
}
|
|
|
|
await getDevicePairingLink(ctx)
|
|
|
|
expect(ctx.body).toEqual({
|
|
code: 'pair-secret',
|
|
link: 'https://studio.example.com/#/hermes/devices?pairing_code=pair-secret',
|
|
})
|
|
})
|
|
|
|
it('falls back from localhost to LAN addresses when building copyable pairing links', async () => {
|
|
vi.doMock('os', async () => {
|
|
const actual = await vi.importActual<typeof import('os')>('os')
|
|
return {
|
|
...actual,
|
|
networkInterfaces: () => ({
|
|
lo0: [{ family: 'IPv4', internal: true, address: '127.0.0.1' }],
|
|
en0: [{ family: 'IPv4', internal: false, address: '192.168.1.88' }],
|
|
}),
|
|
}
|
|
})
|
|
vi.doMock('../../packages/server/src/services/device-pairing-code', () => ({
|
|
getDevicePairingCode: () => 'pair-secret',
|
|
verifyDevicePairingCode: () => false,
|
|
}))
|
|
|
|
const { getDevicePairingLink } = await import('../../packages/server/src/controllers/devices')
|
|
const ctx: any = {
|
|
protocol: 'http',
|
|
host: 'localhost:8648',
|
|
get: () => '',
|
|
}
|
|
|
|
await getDevicePairingLink(ctx)
|
|
|
|
expect(ctx.body).toEqual({
|
|
code: 'pair-secret',
|
|
link: 'http://192.168.1.88:8648/#/hermes/devices?pairing_code=pair-secret',
|
|
})
|
|
})
|
|
|
|
it('rejects peer socket connections until outbound pairing is approved locally', async () => {
|
|
vi.doMock('../../packages/server/src/services/lan-discovery', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/lan-discovery')>(
|
|
'../../packages/server/src/services/lan-discovery',
|
|
)
|
|
return {
|
|
...actual,
|
|
getLanDiscoveryCache: () => ({
|
|
scanning: false,
|
|
last_scanned_at: new Date().toISOString(),
|
|
devices: [device],
|
|
}),
|
|
}
|
|
})
|
|
|
|
const { connectPeerDevice } = await import('../../packages/server/src/controllers/devices')
|
|
const ctx: any = {
|
|
params: { id: device.id },
|
|
request: { body: {} },
|
|
}
|
|
|
|
await connectPeerDevice(ctx)
|
|
|
|
expect(ctx.status).toBe(403)
|
|
expect(ctx.body).toEqual({ error: 'Device pairing has not been approved' })
|
|
})
|
|
|
|
it('records outbound status when requesting pairing from a device with inbound history', async () => {
|
|
vi.doMock('../../packages/server/src/services/lan-discovery', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/lan-discovery')>(
|
|
'../../packages/server/src/services/lan-discovery',
|
|
)
|
|
return {
|
|
...actual,
|
|
getLanDiscoveryCache: () => ({
|
|
scanning: false,
|
|
last_scanned_at: new Date().toISOString(),
|
|
devices: [device],
|
|
}),
|
|
}
|
|
})
|
|
vi.doMock('../../packages/server/src/services/system-info', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/system-info')>(
|
|
'../../packages/server/src/services/system-info',
|
|
)
|
|
return {
|
|
...actual,
|
|
getPublicSystemInfo: async () => ({
|
|
device_id: 'hwui_local',
|
|
device_public_key: keyPair.publicKey,
|
|
computer_name: 'local',
|
|
os: { type: 'TestOS', platform: 'linux', release: '1', arch: 'x64' },
|
|
hermes_agent_version: 'v1',
|
|
hermes_web_ui_version: '1',
|
|
}),
|
|
createDeviceSignature: async () => 'signature',
|
|
}
|
|
})
|
|
|
|
const fetchMock = vi.fn(async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ status: 'pending' }),
|
|
}))
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const { requestInboundDeviceLink, updateInboundStatus, getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
requestInboundDeviceLink(device)
|
|
updateInboundStatus(device.id, 'approved')
|
|
|
|
const { requestDevicePairing } = await import('../../packages/server/src/controllers/devices')
|
|
const ctx: any = {
|
|
params: { id: device.id },
|
|
request: { body: {} },
|
|
}
|
|
|
|
await requestDevicePairing(ctx)
|
|
|
|
const relation = getDeviceRelation(device.id)
|
|
expect(ctx.status).toBeUndefined()
|
|
expect(relation?.inbound_status).toBe('approved')
|
|
expect(relation?.outbound_status).toBe('pending')
|
|
expect(relation?.outbound_requested_at).toBeGreaterThan(0)
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'http://192.168.1.20:8648/api/devices/link-request',
|
|
expect.objectContaining({ method: 'POST' }),
|
|
)
|
|
})
|
|
|
|
it('falls back to the native HTTP client when fetch cannot reach a LAN peer', async () => {
|
|
let receivedRequest: any = null
|
|
let server: Server | null = null
|
|
|
|
await new Promise<void>((resolve) => {
|
|
server = createServer((req, res) => {
|
|
const chunks: Buffer[] = []
|
|
req.on('data', chunk => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
|
|
req.on('end', () => {
|
|
if (req.url === '/api/devices/link-request') {
|
|
receivedRequest = JSON.parse(Buffer.concat(chunks).toString('utf-8'))
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({ status: 'pending' }))
|
|
})
|
|
})
|
|
server.listen(0, '127.0.0.1', resolve)
|
|
})
|
|
|
|
const address = server!.address()
|
|
const port = typeof address === 'object' && address ? address.port : 0
|
|
const fallbackDevice: LanDeviceInfo = {
|
|
...device,
|
|
ip: '127.0.0.1',
|
|
http_port: port,
|
|
url: `http://127.0.0.1:${port}`,
|
|
}
|
|
|
|
try {
|
|
vi.doMock('../../packages/server/src/services/lan-discovery', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/lan-discovery')>(
|
|
'../../packages/server/src/services/lan-discovery',
|
|
)
|
|
return {
|
|
...actual,
|
|
getLanDiscoveryCache: () => ({
|
|
scanning: false,
|
|
last_scanned_at: new Date().toISOString(),
|
|
devices: [fallbackDevice],
|
|
}),
|
|
}
|
|
})
|
|
vi.doMock('../../packages/server/src/services/system-info', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/system-info')>(
|
|
'../../packages/server/src/services/system-info',
|
|
)
|
|
return {
|
|
...actual,
|
|
getPublicSystemInfo: async () => ({
|
|
device_id: 'hwui_local',
|
|
device_public_key: keyPair.publicKey,
|
|
computer_name: 'local',
|
|
os: { type: 'TestOS', platform: 'darwin', release: '1', arch: 'arm64' },
|
|
hermes_agent_version: 'v1',
|
|
hermes_web_ui_version: '1',
|
|
}),
|
|
createDeviceSignature: async () => 'signature',
|
|
}
|
|
})
|
|
|
|
const fetchMock = vi.fn(async () => {
|
|
throw Object.assign(new Error(`connect EHOSTUNREACH ${fallbackDevice.ip}:${fallbackDevice.http_port}`), {
|
|
code: 'EHOSTUNREACH',
|
|
syscall: 'connect',
|
|
address: fallbackDevice.ip,
|
|
port: fallbackDevice.http_port,
|
|
})
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
const { requestDevicePairing } = await import('../../packages/server/src/controllers/devices')
|
|
const ctx: any = {
|
|
params: { id: fallbackDevice.id },
|
|
request: { body: {} },
|
|
}
|
|
|
|
await requestDevicePairing(ctx)
|
|
|
|
const relation = getDeviceRelation(fallbackDevice.id)
|
|
expect(ctx.status).toBeUndefined()
|
|
expect(relation?.outbound_status).toBe('pending')
|
|
expect(receivedRequest).toEqual(expect.objectContaining({
|
|
device_id: 'hwui_local',
|
|
signature: 'signature',
|
|
}))
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
`http://127.0.0.1:${port}/api/devices/link-request`,
|
|
expect.objectContaining({ method: 'POST' }),
|
|
)
|
|
} finally {
|
|
await new Promise<void>((resolve, reject) => {
|
|
server!.close(err => err ? reject(err) : resolve())
|
|
})
|
|
}
|
|
})
|
|
|
|
it('requests pairing from a manually entered remote URL', async () => {
|
|
vi.doMock('../../packages/server/src/services/lan-discovery', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/lan-discovery')>(
|
|
'../../packages/server/src/services/lan-discovery',
|
|
)
|
|
return {
|
|
...actual,
|
|
getLanDiscoveryCache: () => ({
|
|
scanning: false,
|
|
last_scanned_at: new Date().toISOString(),
|
|
devices: [],
|
|
}),
|
|
}
|
|
})
|
|
vi.doMock('../../packages/server/src/services/system-info', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/system-info')>(
|
|
'../../packages/server/src/services/system-info',
|
|
)
|
|
return {
|
|
...actual,
|
|
getPublicSystemInfo: async () => ({
|
|
device_id: 'hwui_local',
|
|
device_public_key: keyPair.publicKey,
|
|
computer_name: 'local',
|
|
os: { type: 'TestOS', platform: 'linux', release: '1', arch: 'x64' },
|
|
hermes_agent_version: 'v1',
|
|
hermes_web_ui_version: '1',
|
|
}),
|
|
createDeviceSignature: async () => 'signature',
|
|
}
|
|
})
|
|
|
|
const fetchMock = vi.fn(async (url: string, options?: RequestInit) => {
|
|
if (url === 'https://remote.example.com/api/devices/link-info') {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({
|
|
device_id: device.id,
|
|
device_public_key: device.device_public_key,
|
|
computer_name: 'remote-device',
|
|
endpoint_kind: 'web',
|
|
http_port: 443,
|
|
os: { type: 'Linux', platform: 'linux', release: '1', arch: 'x64' },
|
|
hermes_agent_version: 'v1',
|
|
hermes_web_ui_version: '1',
|
|
}),
|
|
}
|
|
}
|
|
if (url === 'https://remote.example.com/api/devices/link-request' && options?.method === 'POST') {
|
|
expect(JSON.parse(String(options.body))).toEqual(expect.objectContaining({
|
|
pairing_code: 'pair-secret',
|
|
}))
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ status: 'pending' }),
|
|
}
|
|
}
|
|
if (url === 'https://remote.example.com/api/devices/link-status' && options?.method === 'POST') {
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ status: 'pending' }),
|
|
}
|
|
}
|
|
throw new Error(`unexpected fetch: ${url}`)
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const { getDeviceRelation } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
const { requestManualDevicePairing } = await import('../../packages/server/src/controllers/devices')
|
|
const ctx: any = {
|
|
request: {
|
|
body: {
|
|
url: 'https://remote.example.com/#/hermes/devices?pairing_code=pair-secret',
|
|
},
|
|
},
|
|
}
|
|
|
|
await requestManualDevicePairing(ctx)
|
|
|
|
const relation = getDeviceRelation(device.id)
|
|
expect(ctx.status).toBeUndefined()
|
|
expect(relation?.outbound_status).toBe('pending')
|
|
expect(relation?.url).toBe('https://remote.example.com')
|
|
expect(ctx.body.devices).toEqual([
|
|
expect.objectContaining({
|
|
id: device.id,
|
|
computer_name: 'remote-device',
|
|
online: true,
|
|
outbound_status: 'pending',
|
|
url: 'https://remote.example.com',
|
|
}),
|
|
])
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'https://remote.example.com/api/devices/link-info',
|
|
expect.objectContaining({ method: 'GET' }),
|
|
)
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'https://remote.example.com/api/devices/link-request',
|
|
expect.objectContaining({ method: 'POST' }),
|
|
)
|
|
})
|
|
|
|
it('keeps a manually paired remote device in the list when it is offline', async () => {
|
|
vi.doMock('../../packages/server/src/services/lan-discovery', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/lan-discovery')>(
|
|
'../../packages/server/src/services/lan-discovery',
|
|
)
|
|
return {
|
|
...actual,
|
|
getLanDiscoveryCache: () => ({
|
|
scanning: false,
|
|
last_scanned_at: new Date().toISOString(),
|
|
devices: [],
|
|
}),
|
|
}
|
|
})
|
|
vi.doMock('../../packages/server/src/services/system-info', async () => {
|
|
const actual = await vi.importActual<typeof import('../../packages/server/src/services/system-info')>(
|
|
'../../packages/server/src/services/system-info',
|
|
)
|
|
return {
|
|
...actual,
|
|
getPublicSystemInfo: async () => ({
|
|
device_id: 'hwui_local',
|
|
device_public_key: keyPair.publicKey,
|
|
computer_name: 'local',
|
|
os: { type: 'TestOS', platform: 'linux', release: '1', arch: 'x64' },
|
|
hermes_agent_version: 'v1',
|
|
hermes_web_ui_version: '1',
|
|
}),
|
|
createDeviceSignature: async () => 'signature',
|
|
}
|
|
})
|
|
|
|
const fetchMock = vi.fn(async () => {
|
|
throw new Error('offline')
|
|
})
|
|
vi.stubGlobal('fetch', fetchMock)
|
|
|
|
const { updateOutboundStatus } = await import('../../packages/server/src/db/hermes/devices-store')
|
|
const { listDevices } = await import('../../packages/server/src/controllers/devices')
|
|
updateOutboundStatus(device.id, 'approved', {
|
|
...device,
|
|
ip: 'remote.example.com',
|
|
http_port: 443,
|
|
url: 'https://remote.example.com',
|
|
})
|
|
|
|
const ctx: any = {}
|
|
await listDevices(ctx)
|
|
|
|
expect(ctx.body.devices).toEqual([
|
|
expect.objectContaining({
|
|
id: device.id,
|
|
online: false,
|
|
outbound_status: 'approved',
|
|
url: 'https://remote.example.com',
|
|
}),
|
|
])
|
|
expect(fetchMock).toHaveBeenCalledWith(
|
|
'https://remote.example.com/api/devices/link-status',
|
|
expect.objectContaining({ method: 'POST' }),
|
|
)
|
|
})
|
|
})
|