Skip to content

Commit d17c6d3

Browse files
committed
feat(admin): add GET /admin/network-health endpoint
1 parent f615f8d commit d17c6d3

6 files changed

Lines changed: 110 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"nostream": minor
3+
---
4+
5+
feat(admin): add GET /admin/network-health endpoint
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Request, Response } from 'express'
2+
3+
import { IController } from '../../@types/controllers'
4+
import { IRelayProbeSnapshotStore } from '../../@types/relay-probe-snapshot'
5+
6+
export class GetAdminNetworkHealthController implements IController {
7+
public constructor(private readonly snapshotStore: IRelayProbeSnapshotStore) {}
8+
9+
public async handleRequest(_request: Request, response: Response): Promise<void> {
10+
const snapshot = await this.snapshotStore.getLatest()
11+
12+
response.status(200).setHeader('content-type', 'application/json').send({ snapshot })
13+
}
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { RedisAdapter } from '../../adapters/redis-adapter'
2+
import { IController } from '../../@types/controllers'
3+
import { getCacheClient } from '../../cache/client'
4+
import { GetAdminNetworkHealthController } from '../../controllers/admin/get-network-health-controller'
5+
import { RelayProbeSnapshotStore } from '../../utils/relay-probe-snapshot'
6+
7+
export const createGetAdminNetworkHealthController = (): IController => {
8+
const snapshotStore = new RelayProbeSnapshotStore(new RedisAdapter(getCacheClient()))
9+
10+
return new GetAdminNetworkHealthController(snapshotStore)
11+
}

‎src/routes/admin/index.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import express, { json, Router } from 'express'
22

33
import { createGetAdminHealthController } from '../../factories/controllers/get-admin-health-controller-factory'
44
import { createGetAdminMetricsController } from '../../factories/controllers/get-admin-metrics-controller-factory'
5+
import { createGetAdminNetworkHealthController } from '../../factories/controllers/get-admin-network-health-controller-factory'
56
import { createGetAdminSessionController } from '../../factories/controllers/get-admin-session-controller-factory'
67
import { createGetAdminSettingsBackupsController } from '../../factories/controllers/get-admin-settings-backups-controller-factory'
78
import { createGetAdminSettingsController } from '../../factories/controllers/get-admin-settings-controller-factory'
@@ -56,6 +57,12 @@ router.get(
5657
adminAuthMiddleware,
5758
withAdminController(createGetAdminMetricsController),
5859
)
60+
router.get(
61+
'/network-health',
62+
adminRateLimitMiddleware,
63+
adminAuthMiddleware,
64+
withAdminController(createGetAdminNetworkHealthController),
65+
)
5966
router.get(
6067
'/settings',
6168
adminRateLimitMiddleware,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { expect } from 'chai'
2+
import Sinon from 'sinon'
3+
4+
import { IRelayProbeSnapshotStore, RelayProbeRunSnapshot } from '../../../../src/@types/relay-probe-snapshot'
5+
import { GetAdminNetworkHealthController } from '../../../../src/controllers/admin/get-network-health-controller'
6+
7+
describe('GetAdminNetworkHealthController', () => {
8+
let snapshotStore: Sinon.SinonStubbedInstance<IRelayProbeSnapshotStore>
9+
let controller: GetAdminNetworkHealthController
10+
let response: {
11+
status: Sinon.SinonStub
12+
setHeader: Sinon.SinonStub
13+
send: Sinon.SinonStub
14+
}
15+
16+
beforeEach(() => {
17+
snapshotStore = {
18+
saveLatest: Sinon.stub(),
19+
getLatest: Sinon.stub(),
20+
}
21+
22+
controller = new GetAdminNetworkHealthController(snapshotStore)
23+
24+
response = {
25+
status: Sinon.stub().returnsThis(),
26+
setHeader: Sinon.stub().returnsThis(),
27+
send: Sinon.stub().returnsThis(),
28+
}
29+
})
30+
31+
it('returns the latest probe snapshot as JSON', async () => {
32+
const snapshot: RelayProbeRunSnapshot = {
33+
runAt: '2026-01-01T00:00:00.000Z',
34+
targets: ['wss://relay.example.com'],
35+
results: [],
36+
status: 'ok',
37+
}
38+
39+
snapshotStore.getLatest.resolves(snapshot)
40+
41+
await controller.handleRequest({} as any, response as any)
42+
43+
expect(snapshotStore.getLatest).to.have.been.calledOnce
44+
expect(response.status).to.have.been.calledOnceWithExactly(200)
45+
expect(response.setHeader).to.have.been.calledOnceWithExactly('content-type', 'application/json')
46+
expect(response.send).to.have.been.calledOnceWithExactly({ snapshot })
47+
})
48+
49+
it('returns null snapshot when no probe run has been stored yet', async () => {
50+
snapshotStore.getLatest.resolves(null)
51+
52+
await controller.handleRequest({} as any, response as any)
53+
54+
expect(response.send).to.have.been.calledOnceWithExactly({ snapshot: null })
55+
})
56+
})

‎test/unit/routes/admin.spec.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Tag } from '../../../src/@types/base'
77
import { EventKinds, EventTags } from '../../../src/constants/base'
88
import * as getAdminHealthControllerFactory from '../../../src/factories/controllers/get-admin-health-controller-factory'
99
import * as getAdminMetricsControllerFactory from '../../../src/factories/controllers/get-admin-metrics-controller-factory'
10+
import * as getAdminNetworkHealthControllerFactory from '../../../src/factories/controllers/get-admin-network-health-controller-factory'
1011
import * as adminRateLimitMiddleware from '../../../src/handlers/request-handlers/admin-rate-limit-middleware'
1112
import * as rateLimiterMiddleware from '../../../src/handlers/request-handlers/rate-limiter-middleware'
1213
import * as settingsFactory from '../../../src/factories/settings-factory'
@@ -19,6 +20,7 @@ describe('admin router', () => {
1920
const originalAdminPassword = process.env.ADMIN_PASSWORD
2021
let createGetAdminHealthControllerStub: Sinon.SinonStub
2122
let createGetAdminMetricsControllerStub: Sinon.SinonStub
23+
let createGetAdminNetworkHealthControllerStub: Sinon.SinonStub
2224
let createSettingsStub: Sinon.SinonStub
2325
let rateLimiterMiddlewareStub: Sinon.SinonStub
2426
let adminRateLimitMiddlewareStub: Sinon.SinonStub
@@ -67,6 +69,17 @@ describe('admin router', () => {
6769
response.end()
6870
},
6971
} as any)
72+
createGetAdminNetworkHealthControllerStub = Sinon.stub(
73+
getAdminNetworkHealthControllerFactory,
74+
'createGetAdminNetworkHealthController',
75+
).returns({
76+
handleRequest: async (_request: any, response: any) => {
77+
response
78+
.status(200)
79+
.setHeader('content-type', 'application/json')
80+
.send({ snapshot: null })
81+
},
82+
} as any)
7083
createSettingsStub = Sinon.stub(settingsFactory, 'createSettings').returns(settings as any)
7184
const passthrough = async (_request: any, _response: any, next: any) => {
7285
next()
@@ -96,6 +109,7 @@ describe('admin router', () => {
96109
const stopServer = async () => {
97110
createGetAdminHealthControllerStub?.restore()
98111
createGetAdminMetricsControllerStub?.restore()
112+
createGetAdminNetworkHealthControllerStub?.restore()
99113
createSettingsStub?.restore()
100114
rateLimiterMiddlewareStub?.restore()
101115
adminRateLimitMiddlewareStub?.restore()
@@ -175,11 +189,13 @@ describe('admin router', () => {
175189
const sessionResponse = await axios.get(`${baseUrl}/session`, { validateStatus: () => true })
176190
const healthResponse = await axios.get(`${baseUrl}/health`, { validateStatus: () => true })
177191
const metricsResponse = await axios.get(`${baseUrl}/metrics`, { validateStatus: () => true })
192+
const networkHealthResponse = await axios.get(`${baseUrl}/network-health`, { validateStatus: () => true })
178193

179194
expect(sessionResponse.status).to.equal(401)
180195
expect(healthResponse.status).to.equal(401)
181196
expect(metricsResponse.status).to.equal(401)
182-
expect(rateLimiterMiddlewareStub.callCount).to.equal(3)
197+
expect(networkHealthResponse.status).to.equal(401)
198+
expect(rateLimiterMiddlewareStub.callCount).to.equal(4)
183199
})
184200

185201
it('authenticates a protected route with a signed NIP-98 event', async () => {

0 commit comments

Comments
 (0)