diff --git a/package-lock.json b/package-lock.json index a5d0fb5..15aaffc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "readable-stream": "4.4.2", "response-time": "2.3.2", "uuid": "9.0.0", - "ws": "8.17.1" + "ws": "8.18.0" }, "bin": { "dwn-server": "dist/esm/src/main.js" @@ -3251,6 +3251,27 @@ "node": ">= 0.6" } }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", @@ -7857,6 +7878,27 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", @@ -8670,6 +8712,27 @@ "ws": "~8.17.1" } }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -9715,9 +9778,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index a8fc520..853469d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "readable-stream": "4.4.2", "response-time": "2.3.2", "uuid": "9.0.0", - "ws": "8.17.1" + "ws": "8.18.0" }, "devDependencies": { "@types/bytes": "3.1.1", diff --git a/src/config.ts b/src/config.ts index f6ce95a..f42dbb2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,7 +30,9 @@ export const config = { * Postgres: 'postgres://root:dwn@localhost:5432/dwn' * MySQL: 'mysql://root:dwn@localhost:3306/dwn' */ - ttlCacheUrl: process.env.DWN_TTL_CACHE_URL || 'sqlite://', + ttlCacheUrl: process.env.DWN_TTL_CACHE_URL || 'mysql://root:dwn@localhost:3306/dwn', + // ttlCacheUrl: process.env.DWN_TTL_CACHE_URL || 'sqlite://', + // ttlCacheUrl: process.env.DWN_TTL_CACHE_URL || 'postgres://root:dwn@localhost:5432/dwn', /** * Used to populate the `version` and `sdkVersion` properties returned by the `/info` endpoint. diff --git a/src/dwn-server.ts b/src/dwn-server.ts index eb822a4..d7b480d 100644 --- a/src/dwn-server.ts +++ b/src/dwn-server.ts @@ -1,6 +1,7 @@ import type { EventStream } from '@tbd54566975/dwn-sdk-js'; import { Dwn, EventEmitterStream } from '@tbd54566975/dwn-sdk-js'; +import type { ProcessHandlers } from './process-handlers.js'; import type { Server } from 'http'; import log from 'loglevel'; import prefix from 'loglevel-plugin-prefix'; @@ -10,7 +11,7 @@ import { HttpServerShutdownHandler } from './lib/http-server-shutdown-handler.js import { type DwnServerConfig, config as defaultConfig } from './config.js'; import { HttpApi } from './http-api.js'; -import { setProcessHandlers } from './process-handlers.js'; +import { setProcessHandlers, unsetProcessHandlers } from './process-handlers.js'; import { getDWNConfig } from './storage.js'; import { WsApi } from './ws-api.js'; import { RegistrationManager } from './registration/registration-manager.js'; @@ -20,7 +21,14 @@ export type DwnServerOptions = { config?: DwnServerConfig; }; +export enum DwnServerState { + Stopped, + Started +} + export class DwnServer { + serverState = DwnServerState.Stopped; + processHandlers: ProcessHandlers; dwn?: Dwn; config: DwnServerConfig; #httpServerShutdownHandler: HttpServerShutdownHandler; @@ -40,9 +48,17 @@ export class DwnServer { prefix.apply(log); } + /** + * Starts the DWN server. + */ async start(): Promise { + if (this.serverState === DwnServerState.Started) { + return; + } + await this.#setupServer(); - setProcessHandlers(this); + this.processHandlers = setProcessHandlers(this); + this.serverState = DwnServerState.Started; } /** @@ -76,9 +92,8 @@ export class DwnServer { this.#httpApi = await HttpApi.create(this.config, this.dwn, registrationManager); - await this.#httpApi.start(this.config.port, () => { - log.info(`HttpServer listening on port ${this.config.port}`); - }); + await this.#httpApi.start(this.config.port); + log.info(`HttpServer listening on port ${this.config.port}`); this.#httpServerShutdownHandler = new HttpServerShutdownHandler( this.#httpApi.server, @@ -91,8 +106,36 @@ export class DwnServer { } } - stop(callback: () => void): void { - this.#httpServerShutdownHandler.stop(callback); + /** + * Stops the DWN server. + */ + async stop(): Promise { + if (this.serverState === DwnServerState.Stopped) { + return; + } + + // F YEAH! + if (this.dwn['didResolver']['cache']['cache']) { + await this.dwn['didResolver']['cache']['cache']['close'](); + } + + await this.dwn.close(); + await this.#httpApi.stop(); + + // close WebSocket server if it was initialized + if (this.#wsApi !== undefined) { + await this.#wsApi.close(); + } + + await new Promise((resolve) => { + this.#httpServerShutdownHandler.stop(() => { + resolve(); + }); + }); + + unsetProcessHandlers(this.processHandlers); + + this.serverState = DwnServerState.Stopped; } get httpServer(): Server { diff --git a/src/http-api.ts b/src/http-api.ts index 0c6baec..5b7d829 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -340,10 +340,6 @@ export class HttpApi { this.#setupWeb5ConnectServerRoutes(); } - #listen(port: number, callback?: () => void): void { - this.#server.listen(port, callback); - } - #setupRegistrationRoutes(): void { if (this.#config.registrationProofOfWorkEnabled) { this.#api.get('/registration/proof-of-work', async (_req: Request, res: Response) => { @@ -458,8 +454,34 @@ export class HttpApi { }); } - async start(port: number, callback?: () => void): Promise { - this.#listen(port, callback); - return this.#server; + /** + * Starts the HTTP API endpoint on the given port. + * @returns The HTTP server instance. + */ + async start(port: number): Promise { + // promisify http.Server.listen() and await on it + await new Promise((resolve) => { + this.#server.listen(port, () => { + resolve(); + }); + }); + } + + /** + * Stops the HTTP API endpoint. + */ + async stop(): Promise { + // promisify http.Server.close() and await on it + await new Promise((resolve, reject) => { + this.#server.close((err?: Error) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + + this.server.closeAllConnections(); } } diff --git a/src/process-handlers.ts b/src/process-handlers.ts index d458cec..e9f1b66 100644 --- a/src/process-handlers.ts +++ b/src/process-handlers.ts @@ -1,36 +1,66 @@ import type { DwnServer } from './dwn-server.js'; -export const gracefulShutdown = (dwnServer: DwnServer): void => { - dwnServer.stop(() => { - console.log('http server stopped.. exiting'); - process.exit(0); - }); +export const gracefulShutdown = async (dwnServer: DwnServer): Promise => { + await dwnServer.stop(); + console.log('http server stopped.. exiting'); + process.exit(0); }; -export const setProcessHandlers = (dwnServer: DwnServer): void => { - process.on('unhandledRejection', (reason, promise) => { +export type ProcessHandlers = { + unhandledRejectionHandler: (reason: any, promise: Promise) => void, + uncaughtExceptionHandler: (err: Error) => void, + sigintHandler: () => Promise, + sigtermHandler: () => Promise +}; + + +export const setProcessHandlers = (dwnServer: DwnServer): ProcessHandlers => { + const unhandledRejectionHandler = (reason: any, promise: Promise): void => { console.error( `Unhandled promise rejection. Reason: ${reason}. Promise: ${JSON.stringify( promise, )}`, ); - }); + }; - process.on('uncaughtException', (err) => { + const uncaughtExceptionHandler = (err: Error): void => { console.error('Uncaught exception:', err.stack || err); - }); + }; - // triggered by ctrl+c with no traps in between - process.on('SIGINT', async () => { + const sigintHandler = async (): Promise => { console.log('exit signal received [SIGINT]. starting graceful shutdown'); + await gracefulShutdown(dwnServer); + }; - gracefulShutdown(dwnServer); - }); - - // triggered by docker, tiny etc. - process.on('SIGTERM', async () => { + const sigtermHandler = async (): Promise => { console.log('exit signal received [SIGTERM]. starting graceful shutdown'); + await gracefulShutdown(dwnServer); + }; - gracefulShutdown(dwnServer); - }); + process.on('unhandledRejection', unhandledRejectionHandler); + process.on('uncaughtException', uncaughtExceptionHandler); + process.on('SIGINT', sigintHandler); + process.on('SIGTERM', sigtermHandler); + + // Store handlers to be able to remove them later + return { + unhandledRejectionHandler, + uncaughtExceptionHandler, + sigintHandler, + sigtermHandler + }; }; + +export const unsetProcessHandlers = (handlers: ProcessHandlers): void => { + const { + unhandledRejectionHandler, + uncaughtExceptionHandler, + sigintHandler, + sigtermHandler + } = handlers; + + process.off('unhandledRejection', unhandledRejectionHandler); + process.off('uncaughtException', uncaughtExceptionHandler); + process.off('SIGINT', sigintHandler); + process.off('SIGTERM', sigtermHandler); +}; \ No newline at end of file diff --git a/src/registration/registration-store.ts b/src/registration/registration-store.ts index f4d8159..197c38b 100644 --- a/src/registration/registration-store.ts +++ b/src/registration/registration-store.ts @@ -67,6 +67,10 @@ export class RegistrationStore { return result[0]; } + + public async close(): Promise { + this.db.destroy(); + } } interface RegisteredTenants { diff --git a/src/ws-api.ts b/src/ws-api.ts index 426aadc..2cfbd88 100644 --- a/src/ws-api.ts +++ b/src/ws-api.ts @@ -34,9 +34,8 @@ export class WsApi { this.#wsServer.on('close', () => this.#connectionManager.closeAll()); } - start(): WebSocketServer { + start(): void { this.#setupWebSocket(); - return this.#wsServer; } async close(): Promise { diff --git a/tests/connection/connection-manager.spec.ts b/tests/connection/connection-manager.spec.ts index 37f246c..9e62932 100644 --- a/tests/connection/connection-manager.spec.ts +++ b/tests/connection/connection-manager.spec.ts @@ -8,7 +8,6 @@ import { getTestDwn } from '../test-dwn.js'; import { InMemoryConnectionManager } from '../../src/connection/connection-manager.js'; import { config } from '../../src/config.js'; import { WsApi } from '../../src/ws-api.js'; -import type { Server } from 'http'; import { HttpApi } from '../../src/http-api.js'; import { JsonRpcSocket } from '../../src/json-rpc-socket.js'; @@ -17,24 +16,23 @@ chai.use(chaiAsPromised); describe('InMemoryConnectionManager', () => { let dwn: Dwn; let connectionManager: InMemoryConnectionManager; - let server: Server + let httpApi: HttpApi; let wsApi: WsApi; beforeEach(async () => { dwn = await getTestDwn({ withEvents: true }); connectionManager = new InMemoryConnectionManager(dwn); - const httpApi = await HttpApi.create(config, dwn); - server = await httpApi.start(9002); - wsApi = new WsApi(server, dwn, connectionManager); + httpApi = await HttpApi.create(config, dwn); + await httpApi.start(9002); + wsApi = new WsApi(httpApi.server, dwn, connectionManager); wsApi.start(); }); afterEach(async () => { await connectionManager.closeAll(); await dwn.close(); + await httpApi.stop(); await wsApi.close(); - server.close(); - server.closeAllConnections(); sinon.restore(); }); @@ -48,6 +46,7 @@ describe('InMemoryConnectionManager', () => { }); it('closes all connections on `closeAll`', async () => { + await JsonRpcSocket.connect('ws://127.0.0.1:9002'); expect((connectionManager as any).connections.size).to.equal(1); @@ -56,5 +55,5 @@ describe('InMemoryConnectionManager', () => { await connectionManager.closeAll(); expect((connectionManager as any).connections.size).to.equal(0); - }); + }) }); \ No newline at end of file diff --git a/tests/cors.spec.ts b/tests/cors.spec.ts index 9b9649c..1ce7363 100644 --- a/tests/cors.spec.ts +++ b/tests/cors.spec.ts @@ -72,10 +72,9 @@ class CorsProxySetup { resolve(null); }); }); + // shutdown dwn - await new Promise((resolve) => { - dwnServer.stop(resolve); - }); + await dwnServer.stop(); } } diff --git a/tests/dwn-server.spec.ts b/tests/dwn-server.spec.ts index 1f33890..fc6a765 100644 --- a/tests/dwn-server.spec.ts +++ b/tests/dwn-server.spec.ts @@ -16,7 +16,7 @@ describe('DwnServer', function () { const dwnServer = new DwnServer({ config: dwnServerConfig, dwn }); await dwnServer.start(); - dwnServer.stop(() => console.log('server Stop')); + await dwnServer.stop(); expect(dwnServer.httpServer.listening).to.be.false; }); @@ -34,7 +34,9 @@ describe('DwnServer', function () { await withoutSocketServer.start(); expect(withoutSocketServer.httpServer.listening).to.be.true; expect(withoutSocketServer.wsServer).to.be.undefined; - withoutSocketServer.stop(() => console.log('server Stop')); + + await withoutSocketServer.stop(); + console.log('server Stop'); expect(withoutSocketServer.httpServer.listening).to.be.false; }); @@ -50,7 +52,9 @@ describe('DwnServer', function () { await withSocketServer.start(); expect(withSocketServer.wsServer).to.not.be.undefined; - withSocketServer.stop(() => console.log('server Stop')); + + await withSocketServer.stop(); + console.log('server Stop'); expect(withSocketServer.httpServer.listening).to.be.false; }); }); diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index 6e882ee..ced0e1e 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -13,7 +13,6 @@ import { import type { Dwn, DwnError, Persona, ProtocolsConfigureMessage, RecordsQueryReply } from '@tbd54566975/dwn-sdk-js'; import { expect } from 'chai'; -import type { Server } from 'http'; import fetch from 'node-fetch'; import { webcrypto } from 'node:crypto'; import { useFakeTimers } from 'sinon'; @@ -47,7 +46,6 @@ if (!globalThis.crypto) { describe('http api', function () { let httpApi: HttpApi; - let server: Server; let alice: Persona; let registrationManager: RegistrationManager; let dwn: Dwn; @@ -75,7 +73,7 @@ describe('http api', function () { beforeEach(async function () { sinon.restore(); - server = await httpApi.start(3000); + await httpApi.start(3000); // generate a new persona for each test to avoid state pollution alice = await TestDataGenerator.generateDidKeyPersona(); @@ -83,8 +81,7 @@ describe('http api', function () { }); afterEach(async function () { - server.close(); - server.closeAllConnections(); + await httpApi.stop(); }); after(function () { @@ -1060,12 +1057,11 @@ describe('http api', function () { // start server without websocket support enabled - server.close(); - server.closeAllConnections(); + await httpApi.stop(); config.webSocketSupport = false; httpApi = await HttpApi.create(config, dwn, registrationManager); - server = await httpApi.start(3000); + await httpApi.start(3000); resp = await fetch(`http://localhost:3000/info`); expect(resp.status).to.equal(200); @@ -1079,8 +1075,7 @@ describe('http api', function () { }); it('verify /info still returns when package.json file does not exist', async function () { - server.close(); - server.closeAllConnections(); + await httpApi.stop(); // set up spy to check for an error log by the server const logSpy = sinon.spy(log, 'error'); @@ -1089,7 +1084,7 @@ describe('http api', function () { const packageJsonConfig = config.packageJsonPath; config.packageJsonPath = '/some/invalid/file.json'; httpApi = await HttpApi.create(config, dwn, registrationManager); - server = await httpApi.start(3000); + await httpApi.start(3000); const resp = await fetch(`http://localhost:3000/info`); const info = await resp.json(); @@ -1111,14 +1106,13 @@ describe('http api', function () { }); it('verify /info returns server name from config', async function () { - server.close(); - server.closeAllConnections(); + await httpApi.stop(); // set a custom name for the `serverName` const serverName = config.serverName; config.serverName = '@web5/dwn-server-2' httpApi = await HttpApi.create(config, dwn, registrationManager); - server = await httpApi.start(3000); + await httpApi.start(3000); const resp = await fetch(`http://localhost:3000/info`); const info = await resp.json(); diff --git a/tests/poller.ts b/tests/poller.ts new file mode 100644 index 0000000..cddee74 --- /dev/null +++ b/tests/poller.ts @@ -0,0 +1,46 @@ +import { Time } from '@tbd54566975/dwn-sdk-js'; + +export class Poller { + + /** + * The interval in milliseconds to wait before retrying the delegate function. + */ + static pollRetrySleep: number = 20; + + /** + * The maximum time in milliseconds to wait before timing out the delegate function. + */ + static pollTimeout: number = 2000; + + /** + * Polls the delegate function until it succeeds or the timeout is exceeded. + * + * @param delegate a function that returns a promise and may throw. + * @param retrySleep the interval in milliseconds to wait before retrying the delegate function. + * @param timeout the maximum time in milliseconds to wait before timing out the delegate function. + * + * @throws {Error} `Operation timed out` if the timeout is exceeded. + */ + static async pollUntilSuccessOrTimeout( + delegate: () => Promise, + retrySleep: number = Poller.pollRetrySleep, + timeout: number = Poller.pollTimeout, + ): Promise { + const startTime = Date.now(); + + while (true) { + try { + // Attempt to execute the delegate function + return await delegate(); + } catch (error) { + // Check if the timeout has been exceeded + if (Date.now() - startTime >= timeout) { + throw new Error('Operation timed out'); + } + + // Sleep for the retry interval before attempting again + await Time.sleep(retrySleep); + } + } + } +} \ No newline at end of file diff --git a/tests/process-handler.spec.ts b/tests/process-handler.spec.ts index 97dfe4d..9ee7106 100644 --- a/tests/process-handler.spec.ts +++ b/tests/process-handler.spec.ts @@ -4,6 +4,7 @@ import sinon from 'sinon'; import { config } from '../src/config.js'; import { DwnServer } from '../src/dwn-server.js'; import { getTestDwn } from './test-dwn.js'; +import { Poller } from './poller.js'; describe('Process Handlers', function () { let dwnServer: DwnServer; @@ -15,24 +16,33 @@ describe('Process Handlers', function () { await dwnServer.start(); processExitStub = sinon.stub(process, 'exit'); }); - afterEach(async function () { - dwnServer.stop(() => console.log('server stop in Process Handlers tests')); + + afterEach(async () => { + await dwnServer.stop(); + console.log('server stop in Process Handlers tests'); process.removeAllListeners('SIGINT'); process.removeAllListeners('SIGTERM'); process.removeAllListeners('uncaughtException'); processExitStub.restore(); }); + it('should stop when SIGINT is emitted', async function () { process.emit('SIGINT'); - expect(dwnServer.httpServer.listening).to.be.false; - expect(processExitStub.called).to.be.false; // Ensure process.exit is not called + + Poller.pollUntilSuccessOrTimeout(async () => { + expect(dwnServer.httpServer.listening).to.be.false; + expect(processExitStub.called).to.be.false; // Ensure process.exit is not called + }); }); it('should stop when SIGTERM is emitted', async function () { process.emit('SIGTERM'); - expect(dwnServer.httpServer.listening).to.be.false; - expect(processExitStub.called).to.be.false; // Ensure process.exit is not called + + Poller.pollUntilSuccessOrTimeout(async () => { + expect(dwnServer.httpServer.listening).to.be.false; + expect(processExitStub.called).to.be.false; // Ensure process.exit is not called + }); }); it('should log an error for an uncaught exception', function () { @@ -40,6 +50,7 @@ describe('Process Handlers', function () { const errorMessage = 'Test uncaught exception'; const error = new Error(errorMessage); process.emit('uncaughtException', error); + // Ensure console.error was called with the expected error message expect(consoleErrorStub.calledOnce).to.be.true; diff --git a/tests/scenarios/registration.spec.ts b/tests/scenarios/registration.spec.ts index df3e9c7..7e2bc83 100644 --- a/tests/scenarios/registration.spec.ts +++ b/tests/scenarios/registration.spec.ts @@ -1,13 +1,12 @@ -// node.js 18 and earlier, needs globalThis.crypto polyfill -import { DataStream, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; import type { Persona } from '@tbd54566975/dwn-sdk-js'; +import fetch from 'node-fetch'; import { expect } from 'chai'; import { readFileSync } from 'fs'; -import fetch from 'node-fetch'; import { webcrypto } from 'node:crypto'; import { useFakeTimers } from 'sinon'; import { v4 as uuidv4 } from 'uuid'; +import { DataStream, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; import type { DwnServerConfig } from '../../src/config.js'; import { config } from '../../src/config.js'; @@ -30,17 +29,23 @@ import { ProofOfWorkManager } from '../../src/registration/proof-of-work-manager import { DwnServer } from '../../src/dwn-server.js'; import { randomBytes } from 'crypto'; +// node.js 18 and earlier, needs globalThis.crypto polyfill if (!globalThis.crypto) { // @ts-ignore globalThis.crypto = webcrypto; } +console.log = (): void => {}; +console.error = (): void => {}; +console.info = (): void => {}; + describe('Registration scenarios', function () { const dwnMessageEndpoint = 'http://localhost:3000'; const termsOfUseEndpoint = 'http://localhost:3000/registration/terms-of-service'; const proofOfWorkEndpoint = 'http://localhost:3000/registration/proof-of-work'; const registrationEndpoint = 'http://localhost:3000/registration'; + // let didResolverCache = new DidResolverCacheLevel({ location: 'RESOLVERCACHE' }); let alice: Persona; let registrationManager: RegistrationManager; let clock; @@ -64,22 +69,29 @@ describe('Registration scenarios', function () { dwnServerConfig.termsOfServiceFilePath = './tests/fixtures/terms-of-service.txt'; dwnServerConfig.registrationProofOfWorkInitialMaxHash = '0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; // 1 in 16 chance of solving - dwnServer = new DwnServer({ config: dwnServerConfig }); + dwnServer = new DwnServer({ config: dwnServerConfig }); await dwnServer.start(); registrationManager = dwnServer.registrationManager; }); - after(function () { - dwnServer.stop(() => { }); + after(async () => { clock.restore(); }); - beforeEach(function () { - dwnServer.start(); + beforeEach(async () => { + // sinon.restore(); // wipe all previous stubs/spies/mocks/fakes/clock + + // // IMPORTANT: MUST be called AFTER `sinon.restore()` because `sinon.restore()` resets fake timers + // clock = useFakeTimers({ shouldAdvanceTime: true }); + + // dwnServer = new DwnServer({ config: dwnServerConfig }); + // await dwnServer.start(); + // registrationManager = dwnServer.registrationManager; }); - afterEach(function () { - dwnServer.stop(() => {}); + afterEach(async () =>{ + // await dwnServer.registrationManager['registrationStore']['db'].destroy(); + // await dwnServer.stop(); }); it('should facilitate tenant registration with terms-of-service and proof-or-work turned on', async () => { @@ -538,7 +550,7 @@ describe('Registration scenarios', function () { */ it('should initialize ProofOfWorkManager with challenge nonce seed if given.', async function () { - dwnServer.stop(() => {}); + await dwnServer.stop(); const registrationProofOfWorkSeed = randomBytes(32).toString('hex'); const configWithProofOfWorkSeed: DwnServerConfig = { @@ -554,7 +566,8 @@ describe('Registration scenarios', function () { }); it('should allow tenant registration to be turned off to allow all DWN messages through.', async () => { - dwnServer.stop(() => {}); + await dwnServer.stop(); + // await dwnServer.registrationManager['registrationStore']['db'].destroy(); // Scenario: // 1. There is a DWN that does not require tenant registration. @@ -570,13 +583,15 @@ describe('Registration scenarios', function () { await dwnServer.start(); const { jsonRpcRequest, dataBytes } = await generateRecordsWriteJsonRpcRequest(alice); - const writeResponse = await fetch(dwnMessageEndpoint, { + + const writeResponse = await fetch('http://localhost:3002', { method: 'POST', headers: { 'dwn-request': JSON.stringify(jsonRpcRequest), }, body: new Blob([dataBytes]), }); + const writeResponseBody = await writeResponse.json() as JsonRpcResponse; expect(writeResponse.status).to.equal(200); expect(writeResponseBody.result.reply.status.code).to.equal(202); diff --git a/tests/scenarios/web5-connect.spec.ts b/tests/scenarios/web5-connect.spec.ts index d1685f2..0301127 100644 --- a/tests/scenarios/web5-connect.spec.ts +++ b/tests/scenarios/web5-connect.spec.ts @@ -5,7 +5,8 @@ import { DwnServer } from '../../src/dwn-server.js'; import { expect } from 'chai'; import { useFakeTimers } from 'sinon'; import { Web5ConnectServer } from '../../src/web5-connect/web5-connect-server.js'; -import { webcrypto } from 'node:crypto'; +import { randomUUID, webcrypto } from 'node:crypto'; +import { Poller } from '../poller.js'; // node.js 18 and earlier needs globalThis.crypto polyfill if (!globalThis.crypto) { @@ -29,24 +30,23 @@ describe('Web5 Connect scenarios', function () { dwnServerConfig.eventLog = 'sqlite://', dwnServer = new DwnServer({ config: dwnServerConfig }); - await dwnServer.start(); }); - after(function () { - dwnServer.stop(() => { }); + after(async () => { + await dwnServer.stop(); }); - beforeEach(function () { + beforeEach(async () => { sinon.restore(); // wipe all previous stubs/spies/mocks/fakes/clock // IMPORTANT: MUST be called AFTER `sinon.restore()` because `sinon.restore()` resets fake timers clock = useFakeTimers({ shouldAdvanceTime: true }); - dwnServer.start(); + await dwnServer.start(); }); - afterEach(function () { + afterEach(async () => { clock.restore(); - dwnServer.stop(() => {}); + await dwnServer.stop(); }); it('should be able to set and get Web5 Connect Request & Response objects', async () => { @@ -70,18 +70,22 @@ describe('Web5 Connect scenarios', function () { // 2. Identity Provider (wallet) fetches the Web5 Connect Request object from the Web5 Connect server. const requestUrl = (await postWeb5ConnectRequestResult.json() as any).request_uri; - const getWeb5ConnectRequestResult = await fetch(requestUrl, { - method: 'GET', + + let getWeb5ConnectRequestResult; + await Poller.pollUntilSuccessOrTimeout(async () => { + console.log('Polling for Web5 Connect Request object...') + getWeb5ConnectRequestResult = await fetch(requestUrl, { method: 'GET' }); + expect(getWeb5ConnectRequestResult.status).to.equal(200); }); + const fetchedRequest = await getWeb5ConnectRequestResult.json(); - expect(getWeb5ConnectRequestResult.status).to.equal(200); expect(fetchedRequest).to.deep.equal(requestBody.request); // 3. Should receive 404 if fetching the same Web5 Connect Request again - const getWeb5ConnectRequestResult2 = await fetch(requestUrl, { - method: 'GET', + await Poller.pollUntilSuccessOrTimeout(async () => { + const getWeb5ConnectRequestResult2 = await fetch(requestUrl, { method: 'GET' }); + expect(getWeb5ConnectRequestResult2.status).to.equal(404); }); - expect(getWeb5ConnectRequestResult2.status).to.equal(404); // 4. Identity Provider (wallet) should receive 400 if sending an incomplete response. const incompleteResponseBody = { @@ -95,10 +99,11 @@ describe('Web5 Connect scenarios', function () { }); expect(postIncompleteWeb5ConnectResponseResult.status).to.equal(400); + const state = `dummyState-${randomUUID()}`; // 5. Identity Provider (wallet) sends the Web5 Connect Response object to the Web5 Connect server. const web5ConnectResponseBody = { id_token : { dummyToken: 'dummyToken' }, - state : 'dummyState', + state }; const postWeb5ConnectResponseResult = await fetch(`${web5ConnectBaseUrl}/connect/sessions`, { method: 'POST', @@ -109,18 +114,21 @@ describe('Web5 Connect scenarios', function () { // 6. App fetches the Web5 Connect Response object from the Web5 Connect server. const web5ConnectResponseUrl = `${web5ConnectBaseUrl}/connect/sessions/${web5ConnectResponseBody.state}.jwt`; - const getWeb5ConnectResponseResult = await fetch(web5ConnectResponseUrl, { - method: 'GET', + + let getWeb5ConnectResponseResult; + await Poller.pollUntilSuccessOrTimeout(async () => { + getWeb5ConnectResponseResult = await fetch(web5ConnectResponseUrl, { method: 'GET' }); + expect(getWeb5ConnectResponseResult.status).to.equal(200); }); + const fetchedResponse = await getWeb5ConnectResponseResult.json(); - expect(getWeb5ConnectResponseResult.status).to.equal(200); expect(fetchedResponse).to.deep.equal(web5ConnectResponseBody.id_token); // 7. Should receive 404 if fetching the same Web5 Connect Response object again. - const getWeb5ConnectResponseResult2 = await fetch(web5ConnectResponseUrl, { - method: 'GET', + await Poller.pollUntilSuccessOrTimeout(async () => { + const getWeb5ConnectResponseResult2 = await fetch(web5ConnectResponseUrl, { method: 'GET' }); + expect(getWeb5ConnectResponseResult2.status).to.equal(404); }); - expect(getWeb5ConnectResponseResult2.status).to.equal(404); }); it('should clean up objects that are expired', async () => { diff --git a/tests/ws-api.spec.ts b/tests/ws-api.spec.ts index a5a9ef4..5ea4b8f 100644 --- a/tests/ws-api.spec.ts +++ b/tests/ws-api.spec.ts @@ -2,8 +2,6 @@ import type { Dwn, MessageEvent } from '@tbd54566975/dwn-sdk-js'; import { DataStream, Message, TestDataGenerator } from '@tbd54566975/dwn-sdk-js'; -import type { Server } from 'http'; - import { expect } from 'chai'; import { base64url } from 'multiformats/bases/base64'; import type { SinonFakeTimers } from 'sinon'; @@ -24,7 +22,6 @@ import { JsonRpcSocket } from '../src/json-rpc-socket.js'; describe('websocket api', function () { - let server: Server; let httpApi: HttpApi; let wsApi: WsApi; let dwn: Dwn; @@ -41,15 +38,14 @@ describe('websocket api', function () { beforeEach(async function () { dwn = await getTestDwn({ withEvents: true }); httpApi = await HttpApi.create(config, dwn); - server = await httpApi.start(9002); - wsApi = new WsApi(server, dwn); + await httpApi.start(9002); + wsApi = new WsApi(httpApi.server, dwn); wsApi.start(); }); afterEach(async function () { await wsApi.close(); - server.close(); - server.closeAllConnections(); + await httpApi.stop(); await dwn.close(); });