From aeebbd4d86cb62f8d1d53fded65ae40eb5e27910 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Wed, 13 Sep 2023 16:39:42 -0700 Subject: [PATCH] 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