From 5fec555119ece6ccc51664a3eb7d3bc4117a0fd9 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Wed, 13 Sep 2023 13:48:20 -0700 Subject: [PATCH 1/3] feat(cactus-core): add GetOpenApiSpecV1EndpointBase class This is the pre-requisite to finishing the task at https://github.com/hyperledger/cacti/issues/1877 Having this generic endpoint class available will allow us to send in another commit which then uses it to create the Open API spec endpoints in all the plugin classes. Example usage of the generic class looks like this: ```typescript import { GetOpenApiSpecV1EndpointBase, IGetOpenApiSpecV1EndpointBaseOptions, } from "@hyperledger/cactus-core"; import { Checks, LogLevelDesc } from "@hyperledger/cactus-common"; import { IWebServiceEndpoint } from "@hyperledger/cactus-core-api"; import OAS from "../../json/openapi.json"; export const OasPathGetOpenApiSpecV1 = OAS.paths[ "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" ]; export type OasPathTypeGetOpenApiSpecV1 = typeof OasPathGetOpenApiSpecV1; export interface IGetOpenApiSpecV1EndpointOptions extends IGetOpenApiSpecV1EndpointBaseOptions< typeof OAS, OasPathTypeGetOpenApiSpecV1 > { readonly logLevel?: LogLevelDesc; } export class GetOpenApiSpecV1Endpoint extends GetOpenApiSpecV1EndpointBase implements IWebServiceEndpoint { public get className(): string { return GetOpenApiSpecV1Endpoint.CLASS_NAME; } constructor(public readonly options: IGetOpenApiSpecV1EndpointOptions) { super(options); const fnTag = `${this.className}#constructor()`; Checks.truthy(options, `${fnTag} arg options`); } } ``` And the associated OpenAPI specification's paths entry: ```json "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec": { "get": { "x-hyperledger-cactus": { "http": { "verbLowerCase": "get", "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" } }, "operationId": "getOpenApiSpecV1", "summary": "Retrieves the .json file that contains the OpenAPI specification for the plugin.", "parameters": [], "responses": { "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "string" } } } } } } }, ``` Signed-off-by: Peter Somogyvari --- packages/cactus-core/package.json | 1 + .../src/main/typescript/public-api.ts | 4 + .../get-open-api-spec-v1-endpoint-base.ts | 196 ++++++++++++++++++ yarn.lock | 1 + 4 files changed, 202 insertions(+) create mode 100644 packages/cactus-core/src/main/typescript/web-services/get-open-api-spec-v1-endpoint-base.ts diff --git a/packages/cactus-core/package.json b/packages/cactus-core/package.json index d7b112747a..628383037b 100644 --- a/packages/cactus-core/package.json +++ b/packages/cactus-core/package.json @@ -55,6 +55,7 @@ "express": "4.17.3", "express-jwt-authz": "2.4.1", "express-openapi-validator": "5.0.4", + "safe-stable-stringify": "2.4.3", "typescript-optional": "2.0.1" }, "devDependencies": { diff --git a/packages/cactus-core/src/main/typescript/public-api.ts b/packages/cactus-core/src/main/typescript/public-api.ts index b922fe3d24..3871206ef8 100755 --- a/packages/cactus-core/src/main/typescript/public-api.ts +++ b/packages/cactus-core/src/main/typescript/public-api.ts @@ -14,3 +14,7 @@ export { consensusHasTransactionFinality } from "./consensus-has-transaction-fin export { IInstallOpenapiValidationMiddlewareRequest } from "./web-services/install-open-api-validator-middleware"; export { installOpenapiValidationMiddleware } from "./web-services/install-open-api-validator-middleware"; +export { + GetOpenApiSpecV1EndpointBase, + IGetOpenApiSpecV1EndpointBaseOptions, +} from "./web-services/get-open-api-spec-v1-endpoint-base"; diff --git a/packages/cactus-core/src/main/typescript/web-services/get-open-api-spec-v1-endpoint-base.ts b/packages/cactus-core/src/main/typescript/web-services/get-open-api-spec-v1-endpoint-base.ts new file mode 100644 index 0000000000..40b645b0bd --- /dev/null +++ b/packages/cactus-core/src/main/typescript/web-services/get-open-api-spec-v1-endpoint-base.ts @@ -0,0 +1,196 @@ +import type { Express, Request, Response } from "express"; +import { RuntimeError } from "run-time-error"; +import { stringify } from "safe-stable-stringify"; + +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, + IEndpointAuthzOptions, +} from "@hyperledger/cactus-core-api"; + +import { PluginRegistry } from "../plugin-registry"; + +import { registerWebServiceEndpoint } from "./register-web-service-endpoint"; + +export interface IGetOpenApiSpecV1EndpointBaseOptions { + logLevel?: LogLevelDesc; + pluginRegistry: PluginRegistry; + oasPath: P; + oas: S; + path: string; + verbLowerCase: string; + operationId: string; +} + +/** + * A generic base class that plugins can re-use to implement their own endpoints + * which are returning their own OpenAPI specification documents with much less + * boilerplate than otherwise would be needed. + * + * As an example, you can implement a sub-class like this: + * + * ```typescript + * import { + * GetOpenApiSpecV1EndpointBase, + * IGetOpenApiSpecV1EndpointBaseOptions, + * } from "@hyperledger/cactus-core"; + * + * import { Checks, LogLevelDesc } from "@hyperledger/cactus-common"; + * import { IWebServiceEndpoint } from "@hyperledger/cactus-core-api"; + * + * import OAS from "../../json/openapi.json"; + * + * export const OasPathGetOpenApiSpecV1 = + * OAS.paths[ + * "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" + * ]; + * + * export type OasPathTypeGetOpenApiSpecV1 = typeof OasPathGetOpenApiSpecV1; + * + * export interface IGetOpenApiSpecV1EndpointOptions + * extends IGetOpenApiSpecV1EndpointBaseOptions< + * typeof OAS, + * OasPathTypeGetOpenApiSpecV1 + * > { + * readonly logLevel?: LogLevelDesc; + * } + * + * export class GetOpenApiSpecV1Endpoint + * extends GetOpenApiSpecV1EndpointBase + * implements IWebServiceEndpoint + * { + * public get className(): string { + * return GetOpenApiSpecV1Endpoint.CLASS_NAME; + * } + * + * constructor(public readonly options: IGetOpenApiSpecV1EndpointOptions) { + * super(options); + * const fnTag = `${this.className}#constructor()`; + * Checks.truthy(options, `${fnTag} arg options`); + * } + * } + * + * ``` + * + * The above code will also need you to update your openapi.json spec file by + * adding a new endpoint matching it (if you skip this step the compiler should + * complain about missing paths) + * + * ```json + * "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec": { + * "get": { + * "x-hyperledger-cactus": { + * "http": { + * "verbLowerCase": "get", + * "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" + * } + * }, + * "operationId": "getOpenApiSpecV1", + * "summary": "Retrieves the .json file that contains the OpenAPI specification for the plugin.", + * "parameters": [], + * "responses": { + * "200": { + * "description": "OK", + * "content": { + * "application/json": { + * "schema": { + * "type": "string" + * } + * } + * } + * } + * } + * } + * }, + * ``` + */ +export class GetOpenApiSpecV1EndpointBase implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "GetOpenApiSpecV1EndpointBase"; + + protected readonly log: Logger; + + public get className(): string { + return GetOpenApiSpecV1EndpointBase.CLASS_NAME; + } + + constructor( + public readonly opts: IGetOpenApiSpecV1EndpointBaseOptions, + ) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(opts, `${fnTag} arg options`); + Checks.truthy(opts.pluginRegistry, `${fnTag} arg options.pluginRegistry`); + + const level = this.opts.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public get oasPath(): P { + return this.opts.oasPath; + } + + public getPath(): string { + return this.opts.path; + } + + public getVerbLowerCase(): string { + return this.opts.verbLowerCase; + } + + public getOperationId(): string { + return this.opts.operationId; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = `${this.className}#handleRequest()`; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + const reqMeta = `${verbUpper} ${this.getPath()}`; + this.log.debug(reqMeta); + + try { + const { oas } = this.opts; + res.status(200); + res.json(oas); + } catch (ex: unknown) { + const eMsg = `${fnTag} failed to serve request: ${reqMeta}`; + this.log.debug(eMsg, ex); + + const cause = ex instanceof Error ? ex : stringify(ex); + const error = new RuntimeError(eMsg, cause); + + res.status(500).json({ + message: "Internal Server Error", + error, + }); + } + } +} diff --git a/yarn.lock b/yarn.lock index 9a1a721061..5da1530083 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6411,6 +6411,7 @@ __metadata: express: 4.17.3 express-jwt-authz: 2.4.1 express-openapi-validator: 5.0.4 + safe-stable-stringify: 2.4.3 typescript-optional: 2.0.1 uuid: 8.3.2 languageName: unknown From 4e818ca1d94d1bf6a06b44493c32a3b49681c600 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Wed, 13 Sep 2023 14:09:30 -0700 Subject: [PATCH 2/3] feat(connector-besu): add GetOpenApiSpecV1Endpoint (HTTP GET) The new endpoint can serve up the OpenAPI specification file of the plugin via a get request without any parameters needed. Under the hood it uses the re-usable common base endpoint class that we've recently added to the core package. The test case at this file path demonstrates how to use it via the API client: `packages/cactus-plugin-ledger-connector-besu/src/test/typescript/ unit/get-open-api-spec-v1-connector-besu.test.ts` Signed-off-by: Peter Somogyvari --- .../src/main/json/openapi.json | 25 ++++++ .../generated/openapi/typescript-axios/api.ts | 60 +++++++++++++ .../plugin-ledger-connector-besu.ts | 69 ++++++++------ .../get-open-api-spec-v1-endpoint.ts | 39 ++++++++ ...et-open-api-spec-v1-connector-besu.test.ts | 89 +++++++++++++++++++ 5 files changed, 256 insertions(+), 26 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-besu/src/test/typescript/unit/get-open-api-spec-v1-connector-besu.test.ts diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json index 741d568064..ca58c96dbe 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json @@ -949,6 +949,31 @@ } }, "paths": { + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec": { + "get": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "get", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" + } + }, + "operationId": "getOpenApiSpecV1", + "summary": "Retrieves the .json file that contains the OpenAPI specification for the plugin.", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/deploy-contract-solidity-bytecode": { "post": { "x-hyperledger-cactus": { diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts index a5a63011f1..592098eee3 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -1423,6 +1423,36 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Retrieves the .json file that contains the OpenAPI specification for the plugin. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOpenApiSpecV1: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Gets past logs, matching the given options. @@ -1679,6 +1709,16 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getBlockV1(getBlockV1Request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Retrieves the .json file that contains the OpenAPI specification for the plugin. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOpenApiSpecV1(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOpenApiSpecV1(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Gets past logs, matching the given options. @@ -1794,6 +1834,15 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa getBlockV1(getBlockV1Request?: GetBlockV1Request, options?: any): AxiosPromise { return localVarFp.getBlockV1(getBlockV1Request, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Retrieves the .json file that contains the OpenAPI specification for the plugin. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOpenApiSpecV1(options?: any): AxiosPromise { + return localVarFp.getOpenApiSpecV1(options).then((request) => request(axios, basePath)); + }, /** * * @summary Gets past logs, matching the given options. @@ -1911,6 +1960,17 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).getBlockV1(getBlockV1Request, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Retrieves the .json file that contains the OpenAPI specification for the plugin. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getOpenApiSpecV1(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).getOpenApiSpecV1(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Gets past logs, matching the given options. diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts index 4bf16ffa91..fc143f3622 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts @@ -95,6 +95,10 @@ import { RunTransactionEndpoint } from "./web-services/run-transaction-endpoint" import { GetBlockEndpoint } from "./web-services/get-block-v1-endpoint-"; import { GetBesuRecordEndpointV1 } from "./web-services/get-besu-record-endpoint-v1"; import { AbiItem } from "web3-utils"; +import { + GetOpenApiSpecV1Endpoint, + IGetOpenApiSpecV1EndpointOptions, +} from "./web-services/get-open-api-spec-v1-endpoint"; export const E_KEYCHAIN_NOT_FOUND = "cactus.connector.besu.keychain_not_found"; @@ -116,7 +120,8 @@ export class PluginLedgerConnectorBesu RunTransactionResponse >, ICactusPlugin, - IPluginWebService { + IPluginWebService +{ private readonly instanceId: string; public prometheusExporter: PrometheusExporter; private readonly log: Logger; @@ -288,6 +293,26 @@ export class PluginLedgerConnectorBesu const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts); endpoints.push(endpoint); } + { + const oasPath = + OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" + ]; + + const operationId = oasPath.get.operationId; + const opts: IGetOpenApiSpecV1EndpointOptions = { + oas: OAS, + oasPath, + operationId, + path: oasPath.get["x-hyperledger-cactus"].http.path, + pluginRegistry: this.pluginRegistry, + verbLowerCase: oasPath.get["x-hyperledger-cactus"].http.verbLowerCase, + logLevel: this.options.logLevel, + }; + const endpoint = new GetOpenApiSpecV1Endpoint(opts); + endpoints.push(endpoint); + } + this.endpoints = endpoints; return endpoints; } @@ -296,13 +321,12 @@ export class PluginLedgerConnectorBesu return `@hyperledger/cactus-plugin-ledger-connector-besu`; } - public async getConsensusAlgorithmFamily(): Promise< - ConsensusAlgorithmFamily - > { + public async getConsensusAlgorithmFamily(): Promise { return ConsensusAlgorithmFamily.Authority; } public async hasTransactionFinality(): Promise { - const currentConsensusAlgorithmFamily = await this.getConsensusAlgorithmFamily(); + const currentConsensusAlgorithmFamily = + await this.getConsensusAlgorithmFamily(); return consensusHasTransactionFinality(currentConsensusAlgorithmFamily); } @@ -451,18 +475,16 @@ export class PluginLedgerConnectorBesu req.signingCredential.type == Web3SigningCredentialType.CactusKeychainRef ) { - const { - keychainEntryKey, - keychainId, - } = req.signingCredential as Web3SigningCredentialCactusKeychainRef; + const { keychainEntryKey, keychainId } = + req.signingCredential as Web3SigningCredentialCactusKeychainRef; - const keychainPlugin = this.pluginRegistry.findOneByKeychainId( - keychainId, - ); + const keychainPlugin = + this.pluginRegistry.findOneByKeychainId(keychainId); privKey = await keychainPlugin?.get(keychainEntryKey); } else { - privKey = (req.signingCredential as Web3SigningCredentialPrivateKeyHex) - .secret; + privKey = ( + req.signingCredential as Web3SigningCredentialPrivateKeyHex + ).secret; } const fnParams = { @@ -476,9 +498,8 @@ export class PluginLedgerConnectorBesu throw new RuntimeError(`InvalidState: web3Quorum not initialized.`); } - const privacyGroupId = this.web3Quorum.utils.generatePrivacyGroup( - fnParams, - ); + const privacyGroupId = + this.web3Quorum.utils.generatePrivacyGroup(fnParams); this.log.debug("Generated privacyGroupId: ", privacyGroupId); callOutput = await this.web3Quorum.priv.call(privacyGroupId, { to: contractInstance.options.address, @@ -670,7 +691,7 @@ export class PluginLedgerConnectorBesu } return { - transactionReceipt: (txPoolReceipt as unknown) as Web3TransactionReceipt, + transactionReceipt: txPoolReceipt as unknown as Web3TransactionReceipt, }; } @@ -679,9 +700,8 @@ export class PluginLedgerConnectorBesu ): Promise { const fnTag = `${this.className}#transactPrivateKey()`; const { transactionConfig, web3SigningCredential } = req; - const { - secret, - } = web3SigningCredential as Web3SigningCredentialPrivateKeyHex; + const { secret } = + web3SigningCredential as Web3SigningCredentialPrivateKeyHex; // Run transaction to EEA client here if private transaction @@ -727,11 +747,8 @@ export class PluginLedgerConnectorBesu web3SigningCredential, privateTransactionConfig, } = req; - const { - ethAccount, - keychainEntryKey, - keychainId, - } = web3SigningCredential as Web3SigningCredentialCactusKeychainRef; + const { ethAccount, keychainEntryKey, keychainId } = + web3SigningCredential as Web3SigningCredentialCactusKeychainRef; // locate the keychain plugin that has access to the keychain backend // denoted by the keychainID from the request. diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts new file mode 100644 index 0000000000..b3029ae0aa --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts @@ -0,0 +1,39 @@ +import { + GetOpenApiSpecV1EndpointBase, + IGetOpenApiSpecV1EndpointBaseOptions, +} from "@hyperledger/cactus-core"; + +import { Checks, LogLevelDesc } from "@hyperledger/cactus-common"; +import { IWebServiceEndpoint } from "@hyperledger/cactus-core-api"; + +import OAS from "../../json/openapi.json"; + +export const OasPathGetOpenApiSpecV1 = + OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-besu/get-open-api-spec" + ]; + +export type OasPathTypeGetOpenApiSpecV1 = typeof OasPathGetOpenApiSpecV1; + +export interface IGetOpenApiSpecV1EndpointOptions + extends IGetOpenApiSpecV1EndpointBaseOptions< + typeof OAS, + OasPathTypeGetOpenApiSpecV1 + > { + readonly logLevel?: LogLevelDesc; +} + +export class GetOpenApiSpecV1Endpoint + extends GetOpenApiSpecV1EndpointBase + implements IWebServiceEndpoint +{ + public get className(): string { + return GetOpenApiSpecV1Endpoint.CLASS_NAME; + } + + constructor(public readonly options: IGetOpenApiSpecV1EndpointOptions) { + super(options); + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + } +} diff --git a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/unit/get-open-api-spec-v1-connector-besu.test.ts b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/unit/get-open-api-spec-v1-connector-besu.test.ts new file mode 100644 index 0000000000..69a745a6d8 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/unit/get-open-api-spec-v1-connector-besu.test.ts @@ -0,0 +1,89 @@ +import { + IListenOptions, + LogLevelDesc, + LoggerProvider, + Servers, +} from "@hyperledger/cactus-common"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Constants, PluginImportType } from "@hyperledger/cactus-core-api"; +import bodyParser from "body-parser"; +import express from "express"; +import http from "http"; +import "jest-extended"; +import { AddressInfo } from "net"; +import { Server as SocketIoServer } from "socket.io"; +import { v4 as uuidv4 } from "uuid"; +import { + BesuApiClient, + BesuApiClientOptions, + PluginFactoryLedgerConnector, + PluginLedgerConnectorBesu, +} from "../../../main/typescript/public-api"; + +describe(__filename, () => { + const logLevel: LogLevelDesc = "TRACE"; + + const log = LoggerProvider.getOrCreate({ + label: __filename, + level: logLevel, + }); + + const rpcApiHttpHost = "http://127.0.0.1:8000"; + const rpcApiWsHost = "ws://127.0.0.1:9000"; + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + const server = http.createServer(expressApp); + let apiClient: BesuApiClient; + + afterAll(async () => { + await Servers.shutdown(server); + }); + + beforeAll(async () => { + const factory = new PluginFactoryLedgerConnector({ + pluginImportType: PluginImportType.Local, + }); + + const connector: PluginLedgerConnectorBesu = await factory.create({ + rpcApiHttpHost, + rpcApiWsHost, + logLevel, + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins: [] }), + }); + + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + + await connector.registerWebServices(expressApp, wsApi); + + const listenOptions: IListenOptions = { + hostname: "localhost", + port: 0, + server, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + const apiHost = `http://${address}:${port}`; + + const besuApiClientOptions = new BesuApiClientOptions({ + basePath: apiHost, + }); + apiClient = new BesuApiClient(besuApiClientOptions); + log.debug("Instantiated BesuApiClient OK"); + }); + + it("Returns a JSON document containing the Open API specification of the plugin.", async () => { + const res1Promise = apiClient.getOpenApiSpecV1(); + await expect(res1Promise).resolves.not.toThrow(); + const res1 = await res1Promise; + expect(res1.status).toEqual(200); + expect(res1.data).toBeTruthy(); + expect(res1.config).toBeTruthy(); + expect(res1.config.url).toBeString(); + log.debug("Fetched URL OK=%s", res1.config.url); + expect(res1.data).toBeObject(); + }); +}); From 7837390bb7b8605ab87d0dea6893eb4c5a4e4755 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Wed, 13 Sep 2023 16:39:42 -0700 Subject: [PATCH 3/3] feat(cmd-api-server): add GetOpenApiSpecV1Endpoint (HTTP GET) The new endpoint can serve up the OpenAPI specification file of the API server via a get request without any parameters needed. Under the hood it uses the re-usable common base endpoint class that we've recently added to the core package. The test case at this file path demonstrates how to use it via the API client: `packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts` Signed-off-by: Peter Somogyvari --- packages/cactus-cmd-api-server/package.json | 1 + .../src/main/json/openapi.json | 29 ++ .../generated/openapi/kotlin-client/README.md | 1 + .../openapitools/client/apis/DefaultApi.kt | 68 +++ .../openapi/services/default_service.proto | 6 + .../src/main/typescript/api-server.ts | 78 ++-- .../generated/openapi/typescript-axios/api.ts | 56 +++ .../protoc-gen-ts/services/default_service.ts | 80 ++++ .../services/default_service_grpc_pb.d.ts | 17 + .../services/default_service_pb.d.ts | 20 + .../typescript/openapi/get-open-api-spec.ts | 48 +++ .../get-open-api-spec-v1-endpoint.ts | 37 ++ .../grpc/grpc-server-api-server.ts | 48 ++- .../get-open-api-spec-v1-endpoint.test.ts | 402 ++++++++++++++++++ .../api-client-routing-node-to-node.test.ts | 1 + .../get-consortium-jws-endpoint.test.ts | 2 + yarn.lock | 1 + 17 files changed, 853 insertions(+), 42 deletions(-) create mode 100644 packages/cactus-cmd-api-server/src/main/typescript/openapi/get-open-api-spec.ts create mode 100644 packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts create mode 100644 packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts diff --git a/packages/cactus-cmd-api-server/package.json b/packages/cactus-cmd-api-server/package.json index 7f86980245..9f65efcba6 100644 --- a/packages/cactus-cmd-api-server/package.json +++ b/packages/cactus-cmd-api-server/package.json @@ -86,6 +86,7 @@ "prom-client": "13.2.0", "run-time-error": "1.4.0", "rxjs": "7.8.1", + "safe-stable-stringify": "2.4.3", "semver": "7.5.2", "socket.io": "4.5.4", "socket.io-client": "4.5.4", diff --git a/packages/cactus-cmd-api-server/src/main/json/openapi.json b/packages/cactus-cmd-api-server/src/main/json/openapi.json index 53aea86c24..a292548f4c 100644 --- a/packages/cactus-cmd-api-server/src/main/json/openapi.json +++ b/packages/cactus-cmd-api-server/src/main/json/openapi.json @@ -72,6 +72,10 @@ "PrometheusExporterMetricsResponse": { "type": "string", "nullable": false + }, + "GetOpenApiSpecV1EndpointResponse": { + "type": "string", + "nullable": false } } }, @@ -126,6 +130,31 @@ } } } + }, + "/api/v1/api-server/get-open-api-spec": { + "get": { + "description": "Returns the openapi.json document of specific plugin.", + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "get", + "path": "/api/v1/api-server/get-open-api-spec" + } + }, + "operationId": "getOpenApiSpecV1", + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetOpenApiSpecV1EndpointResponse" + } + } + } + } + } + } } } } diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md index 139873331a..5889e95417 100644 --- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md +++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/README.md @@ -45,6 +45,7 @@ All URIs are relative to *http://localhost* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- *DefaultApi* | [**getHealthCheckV1**](docs/DefaultApi.md#gethealthcheckv1) | **GET** /api/v1/api-server/healthcheck | Can be used to verify liveness of an API server instance +*DefaultApi* | [**getOpenApiSpecV1**](docs/DefaultApi.md#getopenapispecv1) | **GET** /api/v1/api-server/get-open-api-spec | *DefaultApi* | [**getPrometheusMetricsV1**](docs/DefaultApi.md#getprometheusmetricsv1) | **GET** /api/v1/api-server/get-prometheus-exporter-metrics | Get the Prometheus Metrics diff --git a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt index 149d4c6e63..d05dc1394b 100644 --- a/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt +++ b/packages/cactus-cmd-api-server/src/main/kotlin/generated/openapi/kotlin-client/src/main/kotlin/org/openapitools/client/apis/DefaultApi.kt @@ -113,6 +113,74 @@ class DefaultApi(basePath: kotlin.String = defaultBasePath, client: OkHttpClient ) } + /** + * + * Returns the openapi.json document of specific plugin. + * @return kotlin.String + * @throws IllegalStateException If the request is not correctly configured + * @throws IOException Rethrows the OkHttp execute method exception + * @throws UnsupportedOperationException If the API returns an informational or redirection response + * @throws ClientException If the API returns a client error response + * @throws ServerException If the API returns a server error response + */ + @Suppress("UNCHECKED_CAST") + @Throws(IllegalStateException::class, IOException::class, UnsupportedOperationException::class, ClientException::class, ServerException::class) + fun getOpenApiSpecV1() : kotlin.String { + val localVarResponse = getOpenApiSpecV1WithHttpInfo() + + return when (localVarResponse.responseType) { + ResponseType.Success -> (localVarResponse as Success<*>).data as kotlin.String + ResponseType.Informational -> throw UnsupportedOperationException("Client does not support Informational responses.") + ResponseType.Redirection -> throw UnsupportedOperationException("Client does not support Redirection responses.") + ResponseType.ClientError -> { + val localVarError = localVarResponse as ClientError<*> + throw ClientException("Client error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse) + } + ResponseType.ServerError -> { + val localVarError = localVarResponse as ServerError<*> + throw ServerException("Server error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse) + } + } + } + + /** + * + * Returns the openapi.json document of specific plugin. + * @return ApiResponse + * @throws IllegalStateException If the request is not correctly configured + * @throws IOException Rethrows the OkHttp execute method exception + */ + @Suppress("UNCHECKED_CAST") + @Throws(IllegalStateException::class, IOException::class) + fun getOpenApiSpecV1WithHttpInfo() : ApiResponse { + val localVariableConfig = getOpenApiSpecV1RequestConfig() + + return request( + localVariableConfig + ) + } + + /** + * To obtain the request config of the operation getOpenApiSpecV1 + * + * @return RequestConfig + */ + fun getOpenApiSpecV1RequestConfig() : RequestConfig { + val localVariableBody = null + val localVariableQuery: MultiValueMap = mutableMapOf() + val localVariableHeaders: MutableMap = mutableMapOf() + localVariableHeaders["Accept"] = "application/json" + + return RequestConfig( + method = RequestMethod.GET, + path = "/api/v1/api-server/get-open-api-spec", + query = localVariableQuery, + headers = localVariableHeaders, + requiresAuthentication = false, + body = localVariableBody + ) + } + /** * Get the Prometheus Metrics * diff --git a/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/services/default_service.proto b/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/services/default_service.proto index b6e0723fbd..115e185037 100644 --- a/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/services/default_service.proto +++ b/packages/cactus-cmd-api-server/src/main/proto/generated/openapi/services/default_service.proto @@ -18,10 +18,16 @@ import "models/health_check_response_pb.proto"; service DefaultService { rpc GetHealthCheckV1 (google.protobuf.Empty) returns (HealthCheckResponsePB); + rpc GetOpenApiSpecV1 (google.protobuf.Empty) returns (GetOpenApiSpecV1Response); + rpc GetPrometheusMetricsV1 (google.protobuf.Empty) returns (GetPrometheusMetricsV1Response); } +message GetOpenApiSpecV1Response { + string data = 1; +} + message GetPrometheusMetricsV1Response { string data = 1; } diff --git a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts index 8d452a7b4e..a7fdcc1806 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/api-server.ts @@ -1,5 +1,8 @@ import type { AddressInfo } from "net"; import type { Server as SecureServer } from "https"; +import type { Request, Response, RequestHandler } from "express"; +import type { ServerOptions as SocketIoServerOptions } from "socket.io"; +import type { Socket as SocketIoSocket } from "socket.io"; import exitHook from "async-exit-hook"; import os from "os"; import path from "path"; @@ -13,7 +16,6 @@ import fs from "fs-extra"; import expressHttpProxy from "express-http-proxy"; import { Server as GrpcServer } from "@grpc/grpc-js"; import { ServerCredentials as GrpcServerCredentials } from "@grpc/grpc-js"; -import type { Application, Request, Response, RequestHandler } from "express"; import express from "express"; import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types"; import compression from "compression"; @@ -22,8 +24,6 @@ import cors from "cors"; import rateLimit from "express-rate-limit"; import { Server as SocketIoServer } from "socket.io"; -import type { ServerOptions as SocketIoServerOptions } from "socket.io"; -import type { Socket as SocketIoSocket } from "socket.io"; import { authorize as authorizeSocket } from "@thream/socketio-jwt"; import { @@ -37,7 +37,11 @@ import { PluginImportAction, } from "@hyperledger/cactus-core-api"; -import { PluginRegistry } from "@hyperledger/cactus-core"; +import { + PluginRegistry, + registerWebServiceEndpoint, +} from "@hyperledger/cactus-core"; + import { installOpenapiValidationMiddleware } from "@hyperledger/cactus-core"; import { @@ -49,7 +53,6 @@ import { import { ICactusApiServerOptions } from "./config/config-service"; import OAS from "../json/openapi.json"; -// import { OpenAPIV3 } from "express-openapi-validator/dist/framework/types"; import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; import { AuthorizerFactory } from "./authzn/authorizer-factory"; @@ -58,6 +61,10 @@ import { WatchHealthcheckV1Endpoint } from "./web-services/watch-healthcheck-v1- import * as default_service from "./generated/proto/protoc-gen-ts/services/default_service"; import { GrpcServerApiServer } from "./web-services/grpc/grpc-server-api-server"; import { determineAddressFamily } from "./common/determine-address-family"; +import { + GetOpenApiSpecV1Endpoint, + IGetOpenApiSpecV1EndpointOptions, +} from "./web-services/get-open-api-spec-v1-endpoint"; export interface IApiServerConstructorOptions { readonly pluginManagerOptions?: { pluginsPath: string }; @@ -91,8 +98,8 @@ export class ApiServer { private readonly httpServerCockpit?: Server | SecureServer; private readonly wsApi: SocketIoServer; private readonly grpcServer: GrpcServer; - private readonly expressApi: Application; - private readonly expressCockpit: Application; + private readonly expressApi: express.Express; + private readonly expressCockpit: express.Express; private readonly pluginsPath: string; private readonly enableShutdownHook: boolean; @@ -305,17 +312,19 @@ export class ApiServer { } } - public async initPluginRegistry(): Promise { - const registry = new PluginRegistry({ plugins: [] }); + public async initPluginRegistry(req?: { + readonly pluginRegistry: PluginRegistry; + }): Promise { + const { pluginRegistry = new PluginRegistry({ plugins: [] }) } = req || {}; const { plugins } = this.options.config; this.log.info(`Instantiated empty registry, invoking plugin factories...`); for (const pluginImport of plugins) { - const plugin = await this.instantiatePlugin(pluginImport, registry); - registry.add(plugin); + const plugin = await this.instantiatePlugin(pluginImport, pluginRegistry); + pluginRegistry.add(plugin); } - return registry; + return pluginRegistry; } private async instantiatePlugin( @@ -347,7 +356,8 @@ export class ApiServer { // eslint-disable-next-line @typescript-eslint/no-var-requires const pluginPackage = require(/* webpackIgnore: true */ packagePath); - const createPluginFactory = pluginPackage.createPluginFactory as PluginFactoryFactory; + const createPluginFactory = + pluginPackage.createPluginFactory as PluginFactoryFactory; const pluginFactoryOptions: IPluginFactoryOptions = { pluginImportType: pluginImport.type, }; @@ -550,9 +560,27 @@ export class ApiServer { * healthcheck and monitoring information. * @param app */ - async getOrCreateWebServices(app: express.Application): Promise { + async getOrCreateWebServices(app: express.Express): Promise { const { log } = this; const { logLevel } = this.options.config; + const pluginRegistry = await this.getOrInitPluginRegistry(); + + { + const oasPath = OAS.paths["/api/v1/api-server/get-open-api-spec"]; + + const operationId = oasPath.get.operationId; + const opts: IGetOpenApiSpecV1EndpointOptions = { + oas: OAS, + oasPath, + operationId, + path: oasPath.get["x-hyperledger-cactus"].http.path, + pluginRegistry, + verbLowerCase: oasPath.get["x-hyperledger-cactus"].http.verbLowerCase, + logLevel, + }; + const endpoint = new GetOpenApiSpecV1Endpoint(opts); + await registerWebServiceEndpoint(app, endpoint); + } const healthcheckHandler = (req: Request, res: Response) => { res.json({ @@ -596,13 +624,10 @@ export class ApiServer { const { "/api/v1/api-server/get-prometheus-exporter-metrics": oasPathPrometheus, } = OAS.paths; - const { http: httpPrometheus } = oasPathPrometheus.get[ - "x-hyperledger-cactus" - ]; - const { - path: httpPathPrometheus, - verbLowerCase: httpVerbPrometheus, - } = httpPrometheus; + const { http: httpPrometheus } = + oasPathPrometheus.get["x-hyperledger-cactus"]; + const { path: httpPathPrometheus, verbLowerCase: httpVerbPrometheus } = + httpPrometheus; (app as any)[httpVerbPrometheus]( httpPathPrometheus, prometheusExporterHandler, @@ -628,6 +653,12 @@ export class ApiServer { ) : GrpcServerCredentials.createInsecure(); + this.grpcServer.addService( + default_service.org.hyperledger.cactus.cmd_api_server + .DefaultServiceClient.service, + new GrpcServerApiServer(), + ); + this.grpcServer.bindAsync( grpcHostAndPort, grpcTlsCredentials, @@ -636,11 +667,6 @@ export class ApiServer { this.log.error("Binding gRPC failed: ", error); return reject(new RuntimeError("Binding gRPC failed: ", error)); } - this.grpcServer.addService( - default_service.org.hyperledger.cactus.cmd_api_server - .UnimplementedDefaultServiceService.definition, - new GrpcServerApiServer(), - ); this.grpcServer.start(); const family = determineAddressFamily(grpcHost); resolve({ address: grpcHost, port, family }); diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts index 66c5d2c1be..216a4a69e5 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -130,6 +130,35 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Returns the openapi.json document of specific plugin. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOpenApiSpecV1: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/api/v1/api-server/get-open-api-spec`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -189,6 +218,15 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getHealthCheckV1(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * Returns the openapi.json document of specific plugin. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getOpenApiSpecV1(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getOpenApiSpecV1(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Get the Prometheus Metrics @@ -218,6 +256,14 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa getHealthCheckV1(options?: any): AxiosPromise { return localVarFp.getHealthCheckV1(options).then((request) => request(axios, basePath)); }, + /** + * Returns the openapi.json document of specific plugin. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getOpenApiSpecV1(options?: any): AxiosPromise { + return localVarFp.getOpenApiSpecV1(options).then((request) => request(axios, basePath)); + }, /** * * @summary Get the Prometheus Metrics @@ -248,6 +294,16 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).getHealthCheckV1(options).then((request) => request(this.axios, this.basePath)); } + /** + * Returns the openapi.json document of specific plugin. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public getOpenApiSpecV1(options?: AxiosRequestConfig) { + return DefaultApiFp(this.configuration).getOpenApiSpecV1(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Get the Prometheus Metrics diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service.ts index 5e9c1b4cb5..06d8bf0eb1 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service.ts @@ -8,6 +8,73 @@ import * as dependency_2 from "./../models/health_check_response_pb"; import * as pb_1 from "google-protobuf"; import * as grpc_1 from "@grpc/grpc-js"; export namespace org.hyperledger.cactus.cmd_api_server { + export class GetOpenApiSpecV1Response extends pb_1.Message { + #one_of_decls: number[][] = []; + constructor(data?: any[] | { + data?: string; + }) { + super(); + pb_1.Message.initialize(this, Array.isArray(data) ? data : [], 0, -1, [], this.#one_of_decls); + if (!Array.isArray(data) && typeof data == "object") { + if ("data" in data && data.data != undefined) { + this.data = data.data; + } + } + } + get data() { + return pb_1.Message.getFieldWithDefault(this, 1, "") as string; + } + set data(value: string) { + pb_1.Message.setField(this, 1, value); + } + static fromObject(data: { + data?: string; + }): GetOpenApiSpecV1Response { + const message = new GetOpenApiSpecV1Response({}); + if (data.data != null) { + message.data = data.data; + } + return message; + } + toObject() { + const data: { + data?: string; + } = {}; + if (this.data != null) { + data.data = this.data; + } + return data; + } + serialize(): Uint8Array; + serialize(w: pb_1.BinaryWriter): void; + serialize(w?: pb_1.BinaryWriter): Uint8Array | void { + const writer = w || new pb_1.BinaryWriter(); + if (this.data.length) + writer.writeString(1, this.data); + if (!w) + return writer.getResultBuffer(); + } + static deserialize(bytes: Uint8Array | pb_1.BinaryReader): GetOpenApiSpecV1Response { + const reader = bytes instanceof pb_1.BinaryReader ? bytes : new pb_1.BinaryReader(bytes), message = new GetOpenApiSpecV1Response(); + while (reader.nextField()) { + if (reader.isEndGroup()) + break; + switch (reader.getFieldNumber()) { + case 1: + message.data = reader.readString(); + break; + default: reader.skipField(); + } + } + return message; + } + serializeBinary(): Uint8Array { + return this.serialize(); + } + static deserializeBinary(bytes: Uint8Array): GetOpenApiSpecV1Response { + return GetOpenApiSpecV1Response.deserialize(bytes); + } + } export class GetPrometheusMetricsV1Response extends pb_1.Message { #one_of_decls: number[][] = []; constructor(data?: any[] | { @@ -110,6 +177,15 @@ export namespace org.hyperledger.cactus.cmd_api_server { responseSerialize: (message: dependency_2.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB) => Buffer.from(message.serialize()), responseDeserialize: (bytes: Buffer) => dependency_2.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB.deserialize(new Uint8Array(bytes)) }, + GetOpenApiSpecV1: { + path: "/org.hyperledger.cactus.cmd_api_server.DefaultService/GetOpenApiSpecV1", + requestStream: false, + responseStream: false, + requestSerialize: (message: dependency_1.google.protobuf.Empty) => Buffer.from(message.serialize()), + requestDeserialize: (bytes: Buffer) => dependency_1.google.protobuf.Empty.deserialize(new Uint8Array(bytes)), + responseSerialize: (message: GetOpenApiSpecV1Response) => Buffer.from(message.serialize()), + responseDeserialize: (bytes: Buffer) => GetOpenApiSpecV1Response.deserialize(new Uint8Array(bytes)) + }, GetPrometheusMetricsV1: { path: "/org.hyperledger.cactus.cmd_api_server.DefaultService/GetPrometheusMetricsV1", requestStream: false, @@ -122,6 +198,7 @@ export namespace org.hyperledger.cactus.cmd_api_server { }; [method: string]: grpc_1.UntypedHandleCall; abstract GetHealthCheckV1(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; + abstract GetOpenApiSpecV1(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; abstract GetPrometheusMetricsV1(call: grpc_1.ServerUnaryCall, callback: grpc_1.sendUnaryData): void; } export class DefaultServiceClient extends grpc_1.makeGenericClientConstructor(UnimplementedDefaultServiceService.definition, "DefaultService", {}) { @@ -131,6 +208,9 @@ export namespace org.hyperledger.cactus.cmd_api_server { GetHealthCheckV1: GrpcUnaryServiceInterface = (message: dependency_1.google.protobuf.Empty, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { return super.GetHealthCheckV1(message, metadata, options, callback); }; + GetOpenApiSpecV1: GrpcUnaryServiceInterface = (message: dependency_1.google.protobuf.Empty, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { + return super.GetOpenApiSpecV1(message, metadata, options, callback); + }; GetPrometheusMetricsV1: GrpcUnaryServiceInterface = (message: dependency_1.google.protobuf.Empty, metadata: grpc_1.Metadata | grpc_1.CallOptions | grpc_1.requestCallback, options?: grpc_1.CallOptions | grpc_1.requestCallback, callback?: grpc_1.requestCallback): grpc_1.ClientUnaryCall => { return super.GetPrometheusMetricsV1(message, metadata, options, callback); }; diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service_grpc_pb.d.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service_grpc_pb.d.ts index e09bbd86da..2270957a15 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service_grpc_pb.d.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service_grpc_pb.d.ts @@ -11,6 +11,7 @@ import * as models_health_check_response_pb_pb from "../models/health_check_resp interface IDefaultServiceService extends grpc.ServiceDefinition { getHealthCheckV1: IDefaultServiceService_IGetHealthCheckV1; + getOpenApiSpecV1: IDefaultServiceService_IGetOpenApiSpecV1; getPrometheusMetricsV1: IDefaultServiceService_IGetPrometheusMetricsV1; } @@ -23,6 +24,15 @@ interface IDefaultServiceService_IGetHealthCheckV1 extends grpc.MethodDefinition responseSerialize: grpc.serialize; responseDeserialize: grpc.deserialize; } +interface IDefaultServiceService_IGetOpenApiSpecV1 extends grpc.MethodDefinition { + path: "/org.hyperledger.cactus.cmd_api_server.DefaultService/GetOpenApiSpecV1"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface IDefaultServiceService_IGetPrometheusMetricsV1 extends grpc.MethodDefinition { path: "/org.hyperledger.cactus.cmd_api_server.DefaultService/GetPrometheusMetricsV1"; requestStream: false; @@ -37,6 +47,7 @@ export const DefaultServiceService: IDefaultServiceService; export interface IDefaultServiceServer extends grpc.UntypedServiceImplementation { getHealthCheckV1: grpc.handleUnaryCall; + getOpenApiSpecV1: grpc.handleUnaryCall; getPrometheusMetricsV1: grpc.handleUnaryCall; } @@ -44,6 +55,9 @@ export interface IDefaultServiceClient { getHealthCheckV1(request: google_protobuf_empty_pb.Empty, callback: (error: grpc.ServiceError | null, response: models_health_check_response_pb_pb.HealthCheckResponsePB) => void): grpc.ClientUnaryCall; getHealthCheckV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: models_health_check_response_pb_pb.HealthCheckResponsePB) => void): grpc.ClientUnaryCall; getHealthCheckV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: models_health_check_response_pb_pb.HealthCheckResponsePB) => void): grpc.ClientUnaryCall; + getOpenApiSpecV1(request: google_protobuf_empty_pb.Empty, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetOpenApiSpecV1Response) => void): grpc.ClientUnaryCall; + getOpenApiSpecV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetOpenApiSpecV1Response) => void): grpc.ClientUnaryCall; + getOpenApiSpecV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetOpenApiSpecV1Response) => void): grpc.ClientUnaryCall; getPrometheusMetricsV1(request: google_protobuf_empty_pb.Empty, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetPrometheusMetricsV1Response) => void): grpc.ClientUnaryCall; getPrometheusMetricsV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetPrometheusMetricsV1Response) => void): grpc.ClientUnaryCall; getPrometheusMetricsV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetPrometheusMetricsV1Response) => void): grpc.ClientUnaryCall; @@ -54,6 +68,9 @@ export class DefaultServiceClient extends grpc.Client implements IDefaultService public getHealthCheckV1(request: google_protobuf_empty_pb.Empty, callback: (error: grpc.ServiceError | null, response: models_health_check_response_pb_pb.HealthCheckResponsePB) => void): grpc.ClientUnaryCall; public getHealthCheckV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: models_health_check_response_pb_pb.HealthCheckResponsePB) => void): grpc.ClientUnaryCall; public getHealthCheckV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: models_health_check_response_pb_pb.HealthCheckResponsePB) => void): grpc.ClientUnaryCall; + public getOpenApiSpecV1(request: google_protobuf_empty_pb.Empty, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetOpenApiSpecV1Response) => void): grpc.ClientUnaryCall; + public getOpenApiSpecV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetOpenApiSpecV1Response) => void): grpc.ClientUnaryCall; + public getOpenApiSpecV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetOpenApiSpecV1Response) => void): grpc.ClientUnaryCall; public getPrometheusMetricsV1(request: google_protobuf_empty_pb.Empty, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetPrometheusMetricsV1Response) => void): grpc.ClientUnaryCall; public getPrometheusMetricsV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetPrometheusMetricsV1Response) => void): grpc.ClientUnaryCall; public getPrometheusMetricsV1(request: google_protobuf_empty_pb.Empty, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: services_default_service_pb.GetPrometheusMetricsV1Response) => void): grpc.ClientUnaryCall; diff --git a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service_pb.d.ts b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service_pb.d.ts index 24d7c00395..2c4bf0b1ee 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service_pb.d.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/generated/proto/protoc-gen-ts/services/default_service_pb.d.ts @@ -8,6 +8,26 @@ import * as jspb from "google-protobuf"; import * as google_protobuf_empty_pb from "google-protobuf/google/protobuf/empty_pb"; import * as models_health_check_response_pb_pb from "../models/health_check_response_pb_pb"; +export class GetOpenApiSpecV1Response extends jspb.Message { + getData(): string; + setData(value: string): GetOpenApiSpecV1Response; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): GetOpenApiSpecV1Response.AsObject; + static toObject(includeInstance: boolean, msg: GetOpenApiSpecV1Response): GetOpenApiSpecV1Response.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: GetOpenApiSpecV1Response, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): GetOpenApiSpecV1Response; + static deserializeBinaryFromReader(message: GetOpenApiSpecV1Response, reader: jspb.BinaryReader): GetOpenApiSpecV1Response; +} + +export namespace GetOpenApiSpecV1Response { + export type AsObject = { + data: string, + } +} + export class GetPrometheusMetricsV1Response extends jspb.Message { getData(): string; setData(value: string): GetPrometheusMetricsV1Response; diff --git a/packages/cactus-cmd-api-server/src/main/typescript/openapi/get-open-api-spec.ts b/packages/cactus-cmd-api-server/src/main/typescript/openapi/get-open-api-spec.ts new file mode 100644 index 0000000000..bf9009fff4 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/typescript/openapi/get-open-api-spec.ts @@ -0,0 +1,48 @@ +import { + Checks, + LogLevelDesc, + LoggerProvider, +} from "@hyperledger/cactus-common"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { + ICactusPlugin, + IPluginWebService, + isIPluginWebService, +} from "@hyperledger/cactus-core-api"; +import type { OpenAPIV3 } from "express-openapi-validator/dist/framework/types"; + +export async function getOpenApiSpecV1(req: { + readonly pluginRegistry: PluginRegistry; + readonly logLevel?: LogLevelDesc; +}): Promise { + const fnTag = `cactus-cmd-api-server/openapi/get-open-api-spec.ts#getOpenApiSpecV1()`; + Checks.truthy(req, `${fnTag} req`); + Checks.truthy(req.pluginRegistry, `${fnTag} req.pluginRegistry`); + const { pluginRegistry, logLevel = "INFO" } = req; + + const log = LoggerProvider.getOrCreate({ + label: fnTag, + level: logLevel, + }); + + const allPlugins = pluginRegistry.getPlugins(); + + log.debug("Pulled a total of %o plugins from registry.", allPlugins.length); + + const webSvcPlugins = allPlugins.filter((p) => isIPluginWebService(p)); + + log.debug("Found %o web service plugins.", webSvcPlugins.length); + + const openApiJsonSpecsPromises = webSvcPlugins.map( + async (plugin: ICactusPlugin) => { + const pkgName = plugin.getPackageName(); + log.debug("Getting OpenAPI spec for %s", pkgName); + const webPlugin = plugin as IPluginWebService; + const openApiSpec = await webPlugin.getOpenApiSpec(); + return openApiSpec as OpenAPIV3.Document; + }, + ); + + const openApiJsonSpecs = await Promise.all(openApiJsonSpecsPromises); + return openApiJsonSpecs; +} diff --git a/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts new file mode 100644 index 0000000000..79b3e6d4b0 --- /dev/null +++ b/packages/cactus-cmd-api-server/src/main/typescript/web-services/get-open-api-spec-v1-endpoint.ts @@ -0,0 +1,37 @@ +import { + GetOpenApiSpecV1EndpointBase, + IGetOpenApiSpecV1EndpointBaseOptions, +} from "@hyperledger/cactus-core"; + +import { Checks, LogLevelDesc } from "@hyperledger/cactus-common"; +import { IWebServiceEndpoint } from "@hyperledger/cactus-core-api"; + +import OAS from "../../json/openapi.json"; + +export const OasPathGetOpenApiSpecV1 = + OAS.paths["/api/v1/api-server/get-open-api-spec"]; + +export type OasPathTypeGetOpenApiSpecV1 = typeof OasPathGetOpenApiSpecV1; + +export interface IGetOpenApiSpecV1EndpointOptions + extends IGetOpenApiSpecV1EndpointBaseOptions< + typeof OAS, + OasPathTypeGetOpenApiSpecV1 + > { + readonly logLevel?: LogLevelDesc; +} + +export class GetOpenApiSpecV1Endpoint + extends GetOpenApiSpecV1EndpointBase + implements IWebServiceEndpoint +{ + public get className(): string { + return GetOpenApiSpecV1Endpoint.CLASS_NAME; + } + + constructor(public readonly options: IGetOpenApiSpecV1EndpointOptions) { + super(options); + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + } +} diff --git a/packages/cactus-cmd-api-server/src/main/typescript/web-services/grpc/grpc-server-api-server.ts b/packages/cactus-cmd-api-server/src/main/typescript/web-services/grpc/grpc-server-api-server.ts index b6ae3f2063..7716710efe 100644 --- a/packages/cactus-cmd-api-server/src/main/typescript/web-services/grpc/grpc-server-api-server.ts +++ b/packages/cactus-cmd-api-server/src/main/typescript/web-services/grpc/grpc-server-api-server.ts @@ -5,6 +5,9 @@ import * as health_check_response_pb from "../../generated/proto/protoc-gen-ts/m import * as memory_usage_pb from "../../generated/proto/protoc-gen-ts/models/memory_usage_pb"; import * as default_service from "../../generated/proto/protoc-gen-ts/services/default_service"; +import OAS from "../../../json/openapi.json"; +import { stringify } from "safe-stable-stringify"; + export class GrpcServerApiServer extends default_service.org.hyperledger.cactus .cmd_api_server.UnimplementedDefaultServiceService { GetHealthCheckV1( @@ -12,21 +15,21 @@ export class GrpcServerApiServer extends default_service.org.hyperledger.cactus Empty, health_check_response_pb.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB >, - callback: requestCallback< - health_check_response_pb.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB - >, + callback: requestCallback, ): void { - const memoryUsage = new memory_usage_pb.org.hyperledger.cactus.cmd_api_server.MemoryUsagePB( - process.memoryUsage(), - ); + const memoryUsage = + new memory_usage_pb.org.hyperledger.cactus.cmd_api_server.MemoryUsagePB( + process.memoryUsage(), + ); - const healthCheckResponse = new health_check_response_pb.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB( - { - success: true, - createdAt: new Date().toJSON(), - memoryUsage, - }, - ); + const healthCheckResponse = + new health_check_response_pb.org.hyperledger.cactus.cmd_api_server.HealthCheckResponsePB( + { + success: true, + createdAt: new Date().toJSON(), + memoryUsage, + }, + ); callback(null, healthCheckResponse); } @@ -35,11 +38,24 @@ export class GrpcServerApiServer extends default_service.org.hyperledger.cactus Empty, default_service.org.hyperledger.cactus.cmd_api_server.GetPrometheusMetricsV1Response >, - callback: requestCallback< - default_service.org.hyperledger.cactus.cmd_api_server.GetPrometheusMetricsV1Response + callback: requestCallback, + ): void { + const res = + new default_service.org.hyperledger.cactus.cmd_api_server.GetPrometheusMetricsV1Response(); + callback(null, res); + } + + GetOpenApiSpecV1( + call: ServerUnaryCall< + Empty, + default_service.org.hyperledger.cactus.cmd_api_server.GetOpenApiSpecV1Response >, + callback: requestCallback, ): void { - const res = new default_service.org.hyperledger.cactus.cmd_api_server.GetPrometheusMetricsV1Response(); + const res = + new default_service.org.hyperledger.cactus.cmd_api_server.GetOpenApiSpecV1Response(); + const specAsJson = stringify(OAS); + res.data = specAsJson; callback(null, res); } } diff --git a/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts new file mode 100644 index 0000000000..7987ddfbed --- /dev/null +++ b/packages/cactus-cmd-api-server/src/test/typescript/unit/get-open-api-spec-v1-endpoint.test.ts @@ -0,0 +1,402 @@ +import { + ApiServer, + ApiServerApiClient, + ApiServerApiClientConfiguration, + AuthorizationProtocol, + ConfigService, + IAuthorizationConfig, +} from "../../../main/typescript/public-api"; +import { + IJoseFittingJwtParams, + LogLevelDesc, +} from "@hyperledger/cactus-common"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Constants } from "@hyperledger/cactus-core-api"; +import type { AuthorizeOptions as SocketIoJwtOptions } from "@thream/socketio-jwt"; +import type { Params as ExpressJwtOptions } from "express-jwt"; +import "jest-extended"; +import { SignJWT, exportSPKI, generateKeyPair } from "jose"; +import path from "path"; +import { v4 as uuidv4 } from "uuid"; + +import { default_service, empty } from "../../../main/typescript/public-api"; +import * as grpc from "@grpc/grpc-js"; +import { GrpcServerApiServer } from "../../../main/typescript/web-services/grpc/grpc-server-api-server"; +import { RuntimeError } from "run-time-error"; + +describe("cmd-api-server:getOpenApiSpecV1Endpoint", () => { + const logLevel: LogLevelDesc = "TRACE"; + let apiServer: ApiServer; + let apiClient: ApiServerApiClient; + let grpcHost: string; + + afterAll(async () => await apiServer.shutdown()); + + beforeAll(async () => { + const jwtKeyPair = await generateKeyPair("RS256", { modulusLength: 4096 }); + const jwtPublicKey = await exportSPKI(jwtKeyPair.publicKey); + const expressJwtOptions: ExpressJwtOptions & IJoseFittingJwtParams = { + algorithms: ["RS256"], + secret: jwtPublicKey, + audience: uuidv4(), + issuer: uuidv4(), + }; + const socketIoJwtOptions: SocketIoJwtOptions = { + secret: jwtPublicKey, + algorithms: ["RS256"], + }; + expect(expressJwtOptions).toBeTruthy(); + + const authorizationConfig: IAuthorizationConfig = { + unprotectedEndpointExemptions: [], + expressJwtOptions, + socketIoJwtOptions, + socketIoPath: Constants.SocketIoConnectionPathV1, + }; + + const pluginsPath = path.join( + __dirname, + "../../../../../../", // walk back up to the project root + ".tmp/test/test-cmd-api-server/get-open-api-spec-v1-endpoint_test/", // the dir path from the root + uuidv4(), // then a random directory to ensure proper isolation + ); + const pluginManagerOptionsJson = JSON.stringify({ pluginsPath }); + + const pluginRegistry = new PluginRegistry({ logLevel }); + + const configService = new ConfigService(); + + const apiSrvOpts = await configService.newExampleConfig(); + apiSrvOpts.logLevel = logLevel; + apiSrvOpts.pluginManagerOptionsJson = pluginManagerOptionsJson; + apiSrvOpts.authorizationProtocol = AuthorizationProtocol.JSON_WEB_TOKEN; + apiSrvOpts.authorizationConfigJson = authorizationConfig; + apiSrvOpts.configFile = ""; + apiSrvOpts.apiCorsDomainCsv = "*"; + apiSrvOpts.apiPort = 0; + apiSrvOpts.cockpitPort = 0; + apiSrvOpts.grpcPort = 0; + apiSrvOpts.apiTlsEnabled = false; + apiSrvOpts.grpcMtlsEnabled = false; + apiSrvOpts.plugins = []; + + const config = await configService.newExampleConfigConvict(apiSrvOpts); + + apiServer = new ApiServer({ + config: config.getProperties(), + pluginRegistry, + }); + + apiServer.initPluginRegistry({ pluginRegistry }); + const startResponsePromise = apiServer.start(); + await expect(startResponsePromise).toResolve(); + const startResponse = await startResponsePromise; + expect(startResponse).toBeTruthy(); + + const { addressInfoApi, addressInfoGrpc } = await startResponsePromise; + const protocol = apiSrvOpts.apiTlsEnabled ? "https" : "http"; + const { address, port } = addressInfoApi; + const apiHost = `${protocol}://${address}:${port}`; + + grpcHost = `${addressInfoGrpc.address}:${addressInfoGrpc.port}`; + + const jwtPayload = { name: "Peter", location: "Albertirsa" }; + const validJwt = await new SignJWT(jwtPayload) + .setProtectedHeader({ alg: "RS256" }) + .setIssuer(expressJwtOptions.issuer) + .setAudience(expressJwtOptions.audience) + .sign(jwtKeyPair.privateKey); + expect(validJwt).toBeTruthy(); + + const validBearerToken = `Bearer ${validJwt}`; + expect(validBearerToken).toBeTruthy(); + + apiClient = new ApiServerApiClient( + new ApiServerApiClientConfiguration({ + basePath: apiHost, + baseOptions: { headers: { Authorization: validBearerToken } }, + logLevel, + }), + ); + }); + + it("HTTP - returns the OpenAPI spec .json document of the API server itself", async () => { + const res1Promise = apiClient.getOpenApiSpecV1(); + await expect(res1Promise).resolves.toHaveProperty("data.openapi"); + const res1 = await res1Promise; + expect(res1.status).toEqual(200); + expect(res1.data).toBeTruthy(); + }); + + it("gRPC - Vanilla Server & Vanilla Client - makeUnaryRequest", async () => { + const clientInsecureCreds = grpc.credentials.createInsecure(); + const serverInsecureCreds = grpc.ServerCredentials.createInsecure(); + + const server = new grpc.Server(); + + server.addService( + default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient + .service, + new GrpcServerApiServer(), + ); + + const res1Promise = new Promise((resolve, reject) => { + server.bindAsync("localhost:0", serverInsecureCreds, (err, port) => { + if (err) { + reject(err); + } else { + server.start(); + + const client = new grpc.Client( + `localhost:${port}`, + clientInsecureCreds, + ); + + client.makeUnaryRequest( + "/org.hyperledger.cactus.cmd_api_server.DefaultService/GetOpenApiSpecV1", + (x) => x, + (y) => y, + Buffer.from([]), + (err3, value) => { + if (err3) { + reject(err3); + } else { + resolve(value); + } + client.close(); + }, + ); + } + }); + }); + + expect(res1Promise).resolves.toBeObject(); + const res1 = await res1Promise; + expect(res1).toBeObject(); + + await new Promise((resolve, reject) => { + server.tryShutdown((err1) => { + if (err1) { + console.error("Failed to shut down test gRPC server: ", err1); + reject(err1); + } else { + resolve(); + } + }); + }); + }); + + it("gRPC - Vanilla Server + Cacti Client - makeUnaryRequest", async () => { + const clientInsecureCreds = grpc.credentials.createInsecure(); + const serverInsecureCreds = grpc.ServerCredentials.createInsecure(); + + const server = new grpc.Server(); + + server.addService( + default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient + .service, + new GrpcServerApiServer(), + ); + + const res1Promise = new Promise((resolve, reject) => { + server.bindAsync("localhost:0", serverInsecureCreds, (err, port) => { + if (err) { + reject(err); + } else { + server.start(); + + const client = + new default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient( + `localhost:${port}`, + clientInsecureCreds, + ); + client.makeUnaryRequest( + "/org.hyperledger.cactus.cmd_api_server.DefaultService/GetOpenApiSpecV1", + (x) => x, + (y) => y, + Buffer.from([]), + (err3, value) => { + if (err3) { + reject(err3); + } else { + resolve(value); + } + client.close(); + }, + ); + } + }); + }); + + expect(res1Promise).resolves.toBeObject(); + const res1 = await res1Promise; + expect(res1).toBeObject(); + + await new Promise((resolve, reject) => { + server.tryShutdown((err1) => { + if (err1) { + console.error("Failed to shut down test gRPC server: ", err1); + reject(err1); + } else { + resolve(); + } + }); + }); + }); + + it("gRPC - Vanilla Server + Cacti Client - GetOpenApiSpecV1", async () => { + const clientInsecureCreds = grpc.credentials.createInsecure(); + const serverInsecureCreds = grpc.ServerCredentials.createInsecure(); + + const server = new grpc.Server(); + + server.addService( + default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient + .service, + new GrpcServerApiServer(), + ); + + const res1Promise = new Promise((resolve, reject) => { + server.bindAsync("localhost:0", serverInsecureCreds, (err, port) => { + const client = + new default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient( + `localhost:${port}`, + clientInsecureCreds, + ); + if (err) { + reject(err); + } else { + server.start(); + + const req = new empty.google.protobuf.Empty(); + client.GetOpenApiSpecV1(req, (err3, value) => { + if (err3) { + reject(err3); + } else { + resolve(value); + } + client.close(); + }); + } + }); + }); + + expect(res1Promise).resolves.toBeObject(); + const res1 = await res1Promise; + expect(res1).toBeObject(); + + await new Promise((resolve, reject) => { + server.tryShutdown((err1) => { + if (err1) { + console.error("Failed to shut down test gRPC server: ", err1); + reject(err1); + } else { + resolve(); + } + }); + }); + }); + + it("gRPC - Cacti Server & Cacti Client - GetOpenApiSpecV1", async () => { + const clientInsecureCreds = grpc.credentials.createInsecure(); + const res1Promise = + new Promise( + (resolve, reject) => { + const deadline = Date.now() + 100; + + const client = + new default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient( + grpcHost, + clientInsecureCreds, + ); + + client.waitForReady(deadline, (err2) => { + if (err2) { + reject(err2); + } else { + const req = new empty.google.protobuf.Empty(); + client.GetOpenApiSpecV1(req, (err3, value) => { + if (err3) { + reject(err3); + } else if (value) { + resolve(value); + } else { + reject( + new RuntimeError("Response object received is falsy."), + ); + } + client.close(); + }); + } + }); + }, + ); + await expect(res1Promise).resolves.toBeObject(); + const res1 = await res1Promise; + expect(res1).toBeTruthy(); + const res1AsString = res1.toString(); + expect(res1AsString).toBeString(); + expect(() => JSON.parse(res1AsString)).not.toThrowError(); + }); + + it("gRPC - Cacti Server + Cacti Client - makeUnaryRequest", async () => { + const clientInsecureCreds = grpc.credentials.createInsecure(); + + const res1Promise = new Promise((resolve, reject) => { + const client = + new default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient( + grpcHost, + clientInsecureCreds, + ); + client.makeUnaryRequest( + "/org.hyperledger.cactus.cmd_api_server.DefaultService/GetOpenApiSpecV1", + (x) => x, + (y) => y, + Buffer.from([]), + (err3, value) => { + if (err3) { + reject(err3); + } else { + resolve(value); + } + client.close(); + }, + ); + }); + + await expect(res1Promise).resolves.toBeObject(); + const res1 = await res1Promise; + expect(res1).toBeObject(); + }); + + it("gRPC - Cacti Server & Cacti Client - GetOpenApiSpecV1 - no manual waitForReady", async () => { + const clientInsecureCreds = grpc.credentials.createInsecure(); + const client = + new default_service.org.hyperledger.cactus.cmd_api_server.DefaultServiceClient( + grpcHost, + clientInsecureCreds, + ); + const res1Promise = + new Promise( + (resolve, reject) => { + const req = new empty.google.protobuf.Empty(); + client.GetOpenApiSpecV1(req, (err3, value) => { + if (err3) { + reject(err3); + } else if (value) { + resolve(value); + } else { + reject(new RuntimeError("Response object received is falsy.")); + } + client.close(); + }); + }, + ); + await expect(res1Promise).resolves.toBeObject(); + const res1 = await res1Promise; + expect(res1).toBeTruthy(); + const res1AsString = res1.toString(); + expect(res1AsString).toBeString(); + expect(() => JSON.parse(res1AsString)).not.toThrowError(); + }); +}); diff --git a/packages/cactus-test-api-client/src/test/typescript/integration/api-client-routing-node-to-node.test.ts b/packages/cactus-test-api-client/src/test/typescript/integration/api-client-routing-node-to-node.test.ts index e4f96492cb..7bf2b46791 100644 --- a/packages/cactus-test-api-client/src/test/typescript/integration/api-client-routing-node-to-node.test.ts +++ b/packages/cactus-test-api-client/src/test/typescript/integration/api-client-routing-node-to-node.test.ts @@ -250,6 +250,7 @@ describe(testCase, () => { apiServerOptions.cockpitPort = 0; apiServerOptions.grpcPort = 0; apiServerOptions.apiTlsEnabled = false; + apiServerOptions.plugins = []; const config = await configService.newExampleConfigConvict( apiServerOptions, ); diff --git a/packages/cactus-test-plugin-consortium-manual/src/test/typescript/integration/plugin-consortium-manual/get-consortium-jws-endpoint.test.ts b/packages/cactus-test-plugin-consortium-manual/src/test/typescript/integration/plugin-consortium-manual/get-consortium-jws-endpoint.test.ts index eec7948f08..fe91e2a1e3 100644 --- a/packages/cactus-test-plugin-consortium-manual/src/test/typescript/integration/plugin-consortium-manual/get-consortium-jws-endpoint.test.ts +++ b/packages/cactus-test-plugin-consortium-manual/src/test/typescript/integration/plugin-consortium-manual/get-consortium-jws-endpoint.test.ts @@ -266,6 +266,7 @@ describe(testCase, () => { apiServerOptions.cockpitPort = 0; apiServerOptions.grpcPort = 0; apiServerOptions.apiTlsEnabled = false; + apiServerOptions.plugins = []; const config = await configService.newExampleConfigConvict( apiServerOptions, ); @@ -317,6 +318,7 @@ describe(testCase, () => { apiServerOptions.cockpitPort = 0; apiServerOptions.grpcPort = 0; apiServerOptions.apiTlsEnabled = false; + apiServerOptions.plugins = []; const config = await configService.newExampleConfigConvict( apiServerOptions, ); diff --git a/yarn.lock b/yarn.lock index 5da1530083..6060708bd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6294,6 +6294,7 @@ __metadata: protobufjs: 7.2.4 run-time-error: 1.4.0 rxjs: 7.8.1 + safe-stable-stringify: 2.4.3 semver: 7.5.2 socket.io: 4.5.4 socket.io-client: 4.5.4