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-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/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(); + }); +}); 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 9a1a721061..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 @@ -6411,6 +6412,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