diff --git a/.changeset/nip66-relay-monitor-integration-tests.md b/.changeset/nip66-relay-monitor-integration-tests.md new file mode 100644 index 00000000..dddf8b25 --- /dev/null +++ b/.changeset/nip66-relay-monitor-integration-tests.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +test(nip66): add integration tests for RelayMonitorWorker snapshot storage diff --git a/test/integration/features/nip-66/nip-66.feature b/test/integration/features/nip-66/nip-66.feature new file mode 100644 index 00000000..e054c428 --- /dev/null +++ b/test/integration/features/nip-66/nip-66.feature @@ -0,0 +1,27 @@ +@nip-66 +Feature: NIP-66 relay monitoring + Scenario: probe run stores latest snapshot in Redis + Given NIP-66 relay monitoring is enabled + And the probe target is "ws://localhost:18808" + When the relay monitor worker completes a probe run + Then the latest probe snapshot in Redis has status "ok" + And the snapshot includes probe results for "ws://localhost:18808" + + Scenario: disabled monitoring does not write a snapshot + Given NIP-66 relay monitoring is disabled + When the relay monitor worker is started + Then the relay monitor worker does not store a probe snapshot + + Scenario: empty targets fall back to info.relay_url + Given NIP-66 relay monitoring is enabled + And no explicit NIP-66 probe targets are configured + When the relay monitor worker completes a probe run + Then the snapshot uses the configured relay URL as its probe target + And the latest probe snapshot in Redis has status "ok" + + Scenario: invalid probe targets are skipped + Given NIP-66 relay monitoring is enabled + And invalid and valid NIP-66 probe targets are configured + When the relay monitor worker completes a probe run + Then the latest probe snapshot in Redis has status "ok" + And the snapshot includes probe results for "ws://localhost:18808" diff --git a/test/integration/features/nip-66/nip-66.feature.ts b/test/integration/features/nip-66/nip-66.feature.ts new file mode 100644 index 00000000..af9e1e85 --- /dev/null +++ b/test/integration/features/nip-66/nip-66.feature.ts @@ -0,0 +1,155 @@ +import EventEmitter from 'events' + +import { After, Before, Given, Then, When } from '@cucumber/cucumber' +import { expect } from 'chai' +import { assocPath, mergeDeepRight, pipe } from 'ramda' + +import { RelayProbeRunSnapshot } from '../../../../src/@types/relay-probe-snapshot' +import { RedisAdapter } from '../../../../src/adapters/redis-adapter' +import { RelayMonitorWorker } from '../../../../src/app/relay-monitor-worker' +import { getCacheClient } from '../../../../src/cache/client' +import { Settings } from '../../../../src/@types/settings' +import { RELAY_PROBE_SNAPSHOT_KEY, RelayProbeSnapshotStore } from '../../../../src/utils/relay-probe-snapshot' +import { SettingsStatic } from '../../../../src/utils/settings' + +const INTEGRATION_RELAY_URL = 'ws://localhost:18808' +const SNAPSHOT_WAIT_MS = 15_000 +const SNAPSHOT_POLL_MS = 100 +const DISABLED_PROBE_WAIT_MS = 500 + +const defaultNip66Settings = { + enabled: true, + probeIntervalSeconds: 60, + targets: [INTEGRATION_RELAY_URL], + timeouts: { + dnsMs: 10_000, + tlsMs: 10_000, + wsRttMs: 10_000, + nip11Ms: 10_000, + }, + dnsCacheTtlSeconds: 300, +} + +let monitorWorker: RelayMonitorWorker | undefined +let snapshotStore: RelayProbeSnapshotStore | undefined +let cacheAdapter: RedisAdapter | undefined +let savedSettings: Settings | undefined + +const waitForSnapshot = async (): Promise => { + const deadline = Date.now() + SNAPSHOT_WAIT_MS + + while (Date.now() < deadline) { + const snapshot = await snapshotStore!.getLatest() + + if (snapshot) { + return snapshot + } + + await new Promise((resolve) => setTimeout(resolve, SNAPSHOT_POLL_MS)) + } + + throw new Error(`Timed out waiting for probe snapshot at ${RELAY_PROBE_SNAPSHOT_KEY}`) +} + +const createMonitorProcess = (): NodeJS.Process => new EventEmitter() as NodeJS.Process + +const startMonitorWorker = (): RelayMonitorWorker => { + const worker = new RelayMonitorWorker( + createMonitorProcess(), + () => SettingsStatic._settings!, + snapshotStore!, + ) + + worker.run() + + return worker +} + +Before({ tags: '@nip-66' }, async function () { + savedSettings = SettingsStatic._settings + cacheAdapter = new RedisAdapter(getCacheClient()) + snapshotStore = new RelayProbeSnapshotStore(cacheAdapter) + await cacheAdapter.deleteKey(RELAY_PROBE_SNAPSHOT_KEY) +}) + +After({ tags: '@nip-66' }, async function () { + monitorWorker?.close() + monitorWorker = undefined + + if (cacheAdapter) { + await cacheAdapter.deleteKey(RELAY_PROBE_SNAPSHOT_KEY) + } + + SettingsStatic._settings = savedSettings + savedSettings = undefined + snapshotStore = undefined + cacheAdapter = undefined +}) + +Given('NIP-66 relay monitoring is disabled', function () { + SettingsStatic._settings = pipe( + assocPath(['nip66'], mergeDeepRight(defaultNip66Settings, { enabled: false })), + assocPath(['info', 'relay_url'], INTEGRATION_RELAY_URL), + )(SettingsStatic._settings) as Settings +}) + +Given('NIP-66 relay monitoring is enabled', function () { + SettingsStatic._settings = pipe( + assocPath(['nip66'], defaultNip66Settings), + assocPath(['info', 'relay_url'], INTEGRATION_RELAY_URL), + )(SettingsStatic._settings) as Settings +}) + +Given('the probe target is {string}', function (target: string) { + SettingsStatic._settings = assocPath(['nip66', 'targets'], [target], SettingsStatic._settings) as Settings +}) + +Given('no explicit NIP-66 probe targets are configured', function () { + SettingsStatic._settings = assocPath(['nip66', 'targets'], [], SettingsStatic._settings) as Settings +}) + +Given('invalid and valid NIP-66 probe targets are configured', function () { + SettingsStatic._settings = assocPath( + ['nip66', 'targets'], + ['not-a-url', INTEGRATION_RELAY_URL], + SettingsStatic._settings, + ) as Settings +}) + +When('the relay monitor worker completes a probe run', async function () { + monitorWorker = startMonitorWorker() + this.parameters.probeSnapshot = await waitForSnapshot() + monitorWorker.close() + monitorWorker = undefined +}) + +When('the relay monitor worker is started', function () { + monitorWorker = startMonitorWorker() +}) + +Then('the relay monitor worker does not store a probe snapshot', async function () { + await new Promise((resolve) => setTimeout(resolve, DISABLED_PROBE_WAIT_MS)) + + const snapshot = await snapshotStore!.getLatest() + expect(snapshot).to.equal(null) +}) + +Then('the latest probe snapshot in Redis has status {string}', function (status: string) { + expect(this.parameters.probeSnapshot.status).to.equal(status) +}) + +Then('the snapshot includes probe results for {string}', function (target: string) { + const snapshot = this.parameters.probeSnapshot as RelayProbeRunSnapshot + + expect(snapshot.targets).to.deep.equal([target]) + expect(snapshot.results).to.have.length(1) + expect(snapshot.results[0].target.relayUrl).to.equal(target) + expect(snapshot.results[0].checkedAt).to.be.a('string') + expect(snapshot.results[0].wsRtt.status).to.equal('ok') +}) + +Then('the snapshot uses the configured relay URL as its probe target', function () { + const snapshot = this.parameters.probeSnapshot as RelayProbeRunSnapshot + + expect(snapshot.targets).to.deep.equal([INTEGRATION_RELAY_URL]) +})