From 7464735c13727a60c8636e41bb0304a82960bdf5 Mon Sep 17 00:00:00 2001 From: Elena Kolevska Date: Wed, 27 Sep 2023 16:09:15 +0100 Subject: [PATCH] feat: Support for the `DAPR_HTTP_ENDPOINT` and `DAPR_GRPC_ENDPOINT` environment variables. Adds support for DAPR_API_TOKEN to gRPC client (#519) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Adds endpoint parsing Signed-off-by: Elena Kolevska * Adds support for the DAPR_HTTP_ENDPOINT environment variable Signed-off-by: Elena Kolevska * Adds tests for endpoint environment variables (HTTP only) Signed-off-by: Elena Kolevska * test: Adds tests for endpoint environment variables (HTTP only) Signed-off-by: Elena Kolevska * fix(style) Linter fixes Signed-off-by: Elena Kolevska * Adds support for dapr-api-token metadata Signed-off-by: Elena Kolevska * Adds support for the DAPR_GRPC_ENDPOINT environment variable Signed-off-by: Elena Kolevska * Fixes linter issues Signed-off-by: Elena Kolevska * Fixes linter issues Signed-off-by: Elena Kolevska * Only add api token interceptor if it’s specified Signed-off-by: Elena Kolevska * Reorganises the code a bit Signed-off-by: Elena Kolevska * Runs pretty-fix Signed-off-by: Elena Kolevska * Adds test for scheme prefix removal for grpc Signed-off-by: Elena Kolevska * Apply suggestions from code review Co-authored-by: Shubham Sharma Signed-off-by: Elena Kolevska * Apply suggestions from code review Co-authored-by: Shubham Sharma Signed-off-by: Elena Kolevska * Adds examples for the parseEndpoint function Signed-off-by: Elena Kolevska * Adds tests for the dapr-api-token metadata in gRPC calls Signed-off-by: Elena Kolevska * Updates after review Signed-off-by: Elena Kolevska * docs: Adds info and examples about the new environment variables to the docs Signed-off-by: Elena Kolevska * Addresses review comments Signed-off-by: Elena Kolevska * Small formatting fix Signed-off-by: Elena Kolevska * Fixes docs Signed-off-by: Elena Kolevska --------- Signed-off-by: Elena Kolevska Signed-off-by: Elena Kolevska Co-authored-by: Shubham Sharma --- .gitignore | 11 +- .../en/js-sdk-docs/js-client/_index.md | 16 + package.json | 7 +- .../Client/GRPCClient/GRPCClient.ts | 65 ++- .../Client/GRPCClient/GRPCClientProxy.ts | 6 +- src/utils/Client.util.ts | 89 +++- src/utils/Settings.util.ts | 10 + test/e2e/common/client.test.ts | 126 ++++++ test/e2e/grpc/clientWithApiToken.test.ts | 57 +++ test/unit/grpc/GRPCClient.test.ts | 28 ++ test/unit/utils/Client.util.test.ts | 382 ++++++++++++++++++ 11 files changed, 769 insertions(+), 28 deletions(-) create mode 100644 test/e2e/grpc/clientWithApiToken.test.ts create mode 100644 test/unit/grpc/GRPCClient.test.ts diff --git a/.gitignore b/.gitignore index feb9f2d7..6c4cc7b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ ### vscode ### -.vscode/* +.vscode !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json @@ -144,4 +144,11 @@ temp/ build/ # version file is auto-generated -src/version.ts \ No newline at end of file +src/version.ts + +# OSX +/.DS_Store + +# JetBrains +/.idea + diff --git a/daprdocs/content/en/js-sdk-docs/js-client/_index.md b/daprdocs/content/en/js-sdk-docs/js-client/_index.md index 9bc95cec..96a01020 100644 --- a/daprdocs/content/en/js-sdk-docs/js-client/_index.md +++ b/daprdocs/content/en/js-sdk-docs/js-client/_index.md @@ -77,6 +77,22 @@ dapr run --app-id example-sdk --app-protocol grpc -- npm run start npm run start:dapr-grpc ``` +### Environment Variables + +You can use the `DAPR_HTTP_ENDPOINT` and `DAPR_GRPC_ENDPOINT` environment variables to set the Dapr Sidecar's HTTP and gRPC endpoints respectively. When these variables are set, the `daprHost` and `daprPort` don't have to be passed to the constructor, the client will parse them automatically out of the provided endpoints. + +```typescript +import { DaprClient, CommunicationProtocol } from "@dapr/dapr"; + +// Using HTTP, when DAPR_HTTP_ENDPOINT is set +const client = new DaprClient(); + +// Using gRPC, when DAPR_GRPC_ENDPOINT is set +const client = new DaprClient({ communicationProtocol: CommunicationProtocol.GRPC }); +``` + +If the environment variables are set, but `daprHost` and `daprPort` values are passed to the constructor, the latter will take precedence over the environment variables. + ## General ### Increasing Body Size diff --git a/package.json b/package.json index be57cc8f..26b01435 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "test:load:http": "TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./test/components -- npm run test:load 'test/load'", "test:e2e": "jest --runInBand --detectOpenHandles", "test:e2e:all": "npm run test:e2e:http; npm run test:e2e:grpc; npm run test:e2e:common", - "test:e2e:grpc": "npm run test:e2e:grpc:client && npm run test:e2e:grpc:server", - "test:e2e:grpc:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/(client).test.ts' ]", - "test:e2e:grpc:server": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --dapr-http-max-request-size 10 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/(server).test.ts' ]", + "test:e2e:grpc": "npm run test:e2e:grpc:client && npm run test:e2e:grpc:server && npm run test:e2e:grpc:clientWithApiToken", + "test:e2e:grpc:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/*client.test.ts' ]", + "test:e2e:grpc:clientWithApiToken": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 DAPR_API_TOKEN=test dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/clientWithApiToken.test.ts' ]", + "test:e2e:grpc:server": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol grpc --app-port 50001 --dapr-grpc-port 50000 --dapr-http-max-request-size 10 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/grpc/*server.test.ts' ]", "test:e2e:http": "npm run test:e2e:http:client && npm run test:e2e:http:server && npm run test:e2e:http:actors", "test:e2e:http:client": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/http/(client).test.ts' ]", "test:e2e:http:server": "npm run prebuild && TEST_SECRET_1=secret_val_1 TEST_SECRET_2=secret_val_2 dapr run --app-id test-suite --app-protocol http --app-port 50001 --dapr-http-port 50000 --dapr-http-max-request-size 10 --components-path ./test/components -- jest --runInBand --detectOpenHandles --testMatch [ '**/test/e2e/http/(server).test.ts' ]", diff --git a/src/implementation/Client/GRPCClient/GRPCClient.ts b/src/implementation/Client/GRPCClient/GRPCClient.ts index 0b991bc4..397f8f14 100644 --- a/src/implementation/Client/GRPCClient/GRPCClient.ts +++ b/src/implementation/Client/GRPCClient/GRPCClient.ts @@ -28,15 +28,18 @@ export default class GRPCClient implements IClient { private readonly client: GrpcDaprClient; private readonly clientCredentials: grpc.ChannelCredentials; private readonly logger: Logger; + private readonly grpcClientOptions: Partial; constructor(options: DaprClientOptions) { this.options = options; this.clientCredentials = this.generateCredentials(); + this.grpcClientOptions = this.generateChannelOptions(); + this.logger = new Logger("GRPCClient", "GRPCClient", options.logger); this.isInitialized = false; this.logger.info(`Opening connection to ${this.options.daprHost}:${this.options.daprPort}`); - this.client = this.generateClient(this.options.daprHost, this.options.daprPort, this.clientCredentials); + this.client = this.generateClient(this.options.daprHost, this.options.daprPort); } async getClient(requiresInitialization = true): Promise { @@ -52,8 +55,20 @@ export default class GRPCClient implements IClient { return this.clientCredentials; } - private generateChannelOptions(): Record { - const options: Record = {}; + getGrpcClientOptions(): grpc.ClientOptions { + return this.grpcClientOptions; + } + + private generateCredentials(): grpc.ChannelCredentials { + if (this.options.daprHost.startsWith("https")) { + return grpc.ChannelCredentials.createSsl(); + } + return grpc.ChannelCredentials.createInsecure(); + } + + private generateChannelOptions(): Partial { + // const options: Record = {}; + let options: Partial = {}; // See: GRPC_ARG_MAX_SEND_MESSAGE_LENGTH, it is in bytes // https://grpc.github.io/grpc/core/group__grpc__arg__keys.html#ga813f94f9ac3174571dd712c96cdbbdc1 @@ -67,20 +82,48 @@ export default class GRPCClient implements IClient { // Add user agent options["grpc.primary_user_agent"] = "dapr-sdk-js/v" + SDK_VERSION; + // Add interceptors if we have an API token + if (this.options.daprApiToken !== "") { + options = { + interceptors: [this.generateInterceptors()], + ...options, + }; + } + return options; } - private generateClient(host: string, port: string, credentials: grpc.ChannelCredentials): GrpcDaprClient { - const options = this.generateChannelOptions(); - const client = new GrpcDaprClient(`${host}:${port}`, credentials, options); + private generateClient(host: string, port: string): GrpcDaprClient { + return new GrpcDaprClient( + GRPCClient.getEndpoint(host, port), + this.getClientCredentials(), + this.getGrpcClientOptions(), + ); + } - return client; + // The grpc client doesn't allow http:// or https:// for grpc connections, + // so we need to remove it, if it exists + static getEndpoint(host: string, port: string): string { + let endpoint = `${host}:${port}`; + const parts = endpoint.split("://"); + if (parts.length > 1 && parts[0].startsWith("http")) { + endpoint = parts[1]; + } + + return endpoint; } - // @todo: look into making secure credentials - private generateCredentials(): grpc.ChannelCredentials { - const credsChannel = grpc.ChannelCredentials.createInsecure(); - return credsChannel; + private generateInterceptors(): (options: any, nextCall: any) => grpc.InterceptingCall { + return (options: any, nextCall: any) => { + return new grpc.InterceptingCall(nextCall(options), { + start: (metadata, listener, next) => { + if (metadata.get("dapr-api-token").length == 0) { + metadata.add("dapr-api-token", this.options.daprApiToken as grpc.MetadataValue); + } + next(metadata, listener); + }, + }); + }; } setIsInitialized(isInitialized: boolean): void { diff --git a/src/implementation/Client/GRPCClient/GRPCClientProxy.ts b/src/implementation/Client/GRPCClient/GRPCClientProxy.ts index f7f4ced6..a9e6a185 100644 --- a/src/implementation/Client/GRPCClient/GRPCClientProxy.ts +++ b/src/implementation/Client/GRPCClient/GRPCClientProxy.ts @@ -60,7 +60,11 @@ export class GRPCClientProxy { this.grpcClientOptions.interceptors = []; } - this.grpcClientOptions.interceptors = [...this.generateInterceptors(), ...this.grpcClientOptions.interceptors]; + this.grpcClientOptions.interceptors = [ + ...this.generateInterceptors(), + ...(this.grpcClient.getGrpcClientOptions().interceptors ?? []), + ...this.grpcClientOptions.interceptors, + ]; const clientCustom = new this.clsProxy( `${this.grpcClient.options.daprHost}:${this.grpcClient.options.daprPort}`, diff --git a/src/utils/Client.util.ts b/src/utils/Client.util.ts index 3865ffe6..0c6c8bc8 100644 --- a/src/utils/Client.util.ts +++ b/src/utils/Client.util.ts @@ -30,7 +30,7 @@ import { Settings } from "./Settings.util"; import { LoggerOptions } from "../types/logger/LoggerOptions"; import { StateConsistencyEnum } from "../enum/StateConsistency.enum"; import { StateConcurrencyEnum } from "../enum/StateConcurrency.enum"; -import { URLSearchParams } from "url"; +import { URL, URLSearchParams } from "url"; /** * Adds metadata to a map. * @param map Input map @@ -253,24 +253,91 @@ function getType(o: any) { /** * Prepares DaprClientOptions for use by the DaprClient/DaprServer. * If the user does not provide a value for a mandatory option, the default value is used. - * @param clientoptions DaprClientOptions + * @param clientOptions DaprClientOptions * @param defaultCommunicationProtocol CommunicationProtocolEnum + * @param defaultLoggerOptions * @returns DaprClientOptions */ export function getClientOptions( - clientoptions: Partial | undefined, + clientOptions: Partial | undefined, defaultCommunicationProtocol: CommunicationProtocolEnum, defaultLoggerOptions: LoggerOptions | undefined, ): DaprClientOptions { - const clientCommunicationProtocol = clientoptions?.communicationProtocol ?? defaultCommunicationProtocol; + const clientCommunicationProtocol = clientOptions?.communicationProtocol ?? defaultCommunicationProtocol; + + // We decide the host/port/endpoint here + let daprEndpoint = ""; + if (clientCommunicationProtocol == CommunicationProtocolEnum.HTTP) { + daprEndpoint = Settings.getDefaultHttpEndpoint(); + } else if (clientCommunicationProtocol == CommunicationProtocolEnum.GRPC) { + daprEndpoint = Settings.getDefaultGrpcEndpoint(); + } + + let host = Settings.getDefaultHost(); + let port = Settings.getDefaultPort(clientCommunicationProtocol); + + if (clientOptions?.daprHost || clientOptions?.daprPort) { + host = clientOptions?.daprHost ?? host; + port = clientOptions?.daprPort ?? port; + } else if (daprEndpoint != "") { + const [scheme, fqdn, p] = parseEndpoint(daprEndpoint); + host = `${scheme}://${fqdn}`; + port = p; + } + return { - daprHost: clientoptions?.daprHost ?? Settings.getDefaultHost(), - daprPort: clientoptions?.daprPort ?? Settings.getDefaultPort(clientCommunicationProtocol), + daprHost: host, + daprPort: port, communicationProtocol: clientCommunicationProtocol, - isKeepAlive: clientoptions?.isKeepAlive, - logger: clientoptions?.logger ?? defaultLoggerOptions, - actor: clientoptions?.actor, - daprApiToken: clientoptions?.daprApiToken, - maxBodySizeMb: clientoptions?.maxBodySizeMb, + isKeepAlive: clientOptions?.isKeepAlive, + logger: clientOptions?.logger ?? defaultLoggerOptions, + actor: clientOptions?.actor, + daprApiToken: clientOptions?.daprApiToken, + maxBodySizeMb: clientOptions?.maxBodySizeMb, }; } + +/** + * Scheme, fqdn and port + */ +type EndpointTuple = [string, string, string]; + +/** + * Parses an endpoint to scheme, fqdn and port + * Examples: + * - http://localhost:3500 -> [http, localhost, 3500] + * - localhost:3500 -> [http, localhost, 3500] + * - :3500 -> [http, localhost, 3500] + * - localhost -> [http, localhost, 80] + * - https://localhost:3500 -> [https, localhost, 3500] + * - [::1]:3500 -> [http, ::1, 3500] + * - [::1] -> [http, ::1, 80] + * - http://[2001:db8:1f70:0:999:de8:7648:6e8]:5000 -> [http, 2001:db8:1f70:0:999:de8:7648:6e8, 5000] + * @throws Error if the address is invalid + * @param address Endpoint address + * @returns EndpointTuple (scheme, fqdn, port) + */ +export function parseEndpoint(address: string): EndpointTuple { + // Prefix with a scheme and host when they're not present, + // because the URL library won't parse it otherwise + if (address.startsWith(":")) { + address = "http://localhost" + address; + } + if (!address.includes("://")) { + address = "http://" + address; + } + + let scheme, fqdn, port: string; + + try { + const myURL = new URL(address); + scheme = myURL.protocol.replace(":", ""); + fqdn = myURL.hostname.replace("[", ""); + fqdn = fqdn.replace("]", ""); + port = myURL.port || (myURL.protocol == "https:" ? "443" : "80"); + } catch (error) { + throw new Error(`Invalid address: ${address}`); + } + + return [scheme, fqdn, port]; +} diff --git a/src/utils/Settings.util.ts b/src/utils/Settings.util.ts index dbc976ec..6c1084ef 100644 --- a/src/utils/Settings.util.ts +++ b/src/utils/Settings.util.ts @@ -20,6 +20,8 @@ export class Settings { private static readonly defaultHttpPort: string = "3500"; private static readonly defaultGrpcAppPort: string = "50000"; private static readonly defaultGrpcPort: string = "50001"; + private static readonly defaultHttpEndpoint: string = ""; + private static readonly defaultGrpcEndpoint: string = ""; private static readonly defaultCommunicationProtocol: CommunicationProtocolEnum = CommunicationProtocolEnum.HTTP; private static readonly defaultKeepAlive: boolean = true; private static readonly defaultStateGetBulkParallelism: number = 10; @@ -85,6 +87,14 @@ export class Settings { return process.env.APP_PORT ?? Settings.defaultGrpcAppPort; } + static getDefaultHttpEndpoint(): string { + return process.env.DAPR_HTTP_ENDPOINT || Settings.defaultHttpEndpoint; + } + + static getDefaultGrpcEndpoint(): string { + return process.env.DAPR_GRPC_ENDPOINT || Settings.defaultGrpcEndpoint; + } + /** * Gets the default port that the application is listening on. * @param communicationProtocolEnum communication protocol diff --git a/test/e2e/common/client.test.ts b/test/e2e/common/client.test.ts index 5f1bdb40..e8b48d01 100644 --- a/test/e2e/common/client.test.ts +++ b/test/e2e/common/client.test.ts @@ -21,6 +21,7 @@ import { } from "../../../src"; import { sleep } from "../../../src/utils/NodeJS.util"; import { LockStatus } from "../../../src/types/lock/UnlockResponse"; +import { Settings } from "../../../src/utils/Settings.util"; const daprHost = "127.0.0.1"; const daprGrpcPort = "50000"; @@ -557,3 +558,128 @@ describe("common/client", () => { }); }); }); + +describe("http/client with environment variables", () => { + let client: DaprClient; + + // We need to start listening on some endpoints already + // this because Dapr is not dynamic and registers endpoints on boot + // we put a timeout of 10s since it takes around 4s for Dapr to boot up + + afterAll(async () => { + await client.stop(); + }); + + it("should give preference to host and port in constructor arguments over endpoint environment variables ", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; + + client = new DaprClient({ + daprHost, + daprPort: daprHttpPort, + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(daprHost); + expect(client.options.daprPort).toEqual(daprHttpPort); + + client = new DaprClient({ + daprHost, + daprPort: daprGrpcPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(daprHost); + expect(client.options.daprPort).toEqual(daprGrpcPort); + }); + + it("should give preference to port with no host in constructor arguments over environment variables ", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; + + client = new DaprClient({ + daprPort: daprHttpPort, + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); + expect(client.options.daprPort).toEqual(daprHttpPort); + + client = new DaprClient({ + daprPort: daprGrpcPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); + expect(client.options.daprPort).toEqual(daprGrpcPort); + }); + + it("should give preference to host with no port in constructor arguments over environment variables ", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; + + client = new DaprClient({ + daprHost: daprHost, + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(daprHost); + expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.HTTP)); + + client = new DaprClient({ + daprHost: daprHost, + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(daprHost); + expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.GRPC)); + }); + + it("should use environment variable endpoint for HTTP", async () => { + process.env.DAPR_HTTP_ENDPOINT = "https://httpdomain.com"; + client = new DaprClient({ + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual("https://httpdomain.com"); + expect(client.options.daprPort).toEqual("443"); + }); + + it("should use environment variable endpoint for GRPC", async () => { + process.env.DAPR_GRPC_ENDPOINT = "https://grpcdomain.com"; + client = new DaprClient({ + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual("https://grpcdomain.com"); + expect(client.options.daprPort).toEqual("443"); + }); + + it("should use default host and port when no other parameters provided", async () => { + process.env.DAPR_HTTP_ENDPOINT = ""; + process.env.DAPR_GRPC_ENDPOINT = ""; + client = new DaprClient({ + communicationProtocol: CommunicationProtocolEnum.HTTP, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); + expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.HTTP)); + + client = new DaprClient({ + communicationProtocol: CommunicationProtocolEnum.GRPC, + isKeepAlive: false, + }); + + expect(client.options.daprHost).toEqual(Settings.getDefaultHost()); + expect(client.options.daprPort).toEqual(Settings.getDefaultPort(CommunicationProtocolEnum.GRPC)); + }); +}); diff --git a/test/e2e/grpc/clientWithApiToken.test.ts b/test/e2e/grpc/clientWithApiToken.test.ts new file mode 100644 index 00000000..64980a10 --- /dev/null +++ b/test/e2e/grpc/clientWithApiToken.test.ts @@ -0,0 +1,57 @@ +/* +Copyright 2022 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import * as grpc from "@grpc/grpc-js"; +import { CommunicationProtocolEnum, DaprClient, LogLevel } from "../../../src"; +import { DaprClient as DaprClientGrpc } from "../../../src/proto/dapr/proto/runtime/v1/dapr_grpc_pb"; +import { Empty } from "google-protobuf/google/protobuf/empty_pb"; +import { InterceptingListener } from "@grpc/grpc-js/build/src/call-stream"; +import { NextCall } from "@grpc/grpc-js/build/src/client-interceptors"; + +const daprHost = "localhost"; +const daprPort = "50000"; // Dapr Sidecar Port of this Example Server + +describe("grpc/client with api token", () => { + it("should send api token as metadata when present", async () => { + const clientWithToken = new DaprClient({ + daprHost, + daprPort, + communicationProtocol: CommunicationProtocolEnum.GRPC, + daprApiToken: "test", + logger: { + level: LogLevel.Debug, + }, + }); + + let mockMetadataRes: grpc.Metadata = new grpc.Metadata(); + const mockInterceptor = jest.fn((options: grpc.InterceptorOptions, nextCall: NextCall): grpc.InterceptingCall => { + return new grpc.InterceptingCall(nextCall(options), { + start: function ( + metadata: grpc.Metadata, + listener: InterceptingListener, + next: (metadata: grpc.Metadata, listener: InterceptingListener | grpc.Listener) => void, + ) { + mockMetadataRes = metadata; + next(metadata, listener); + }, + }); + }); + + const clientProxy = await clientWithToken.proxy.create(DaprClientGrpc, { + interceptors: [mockInterceptor], + }); + + await new Promise((resolve) => clientProxy.getMetadata(new Empty(), resolve)); + expect(mockMetadataRes.get("dapr-api-token")[0]).toBe("test"); + }); +}); diff --git a/test/unit/grpc/GRPCClient.test.ts b/test/unit/grpc/GRPCClient.test.ts new file mode 100644 index 00000000..cfd3c271 --- /dev/null +++ b/test/unit/grpc/GRPCClient.test.ts @@ -0,0 +1,28 @@ +/* +Copyright 2023 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { GRPCClient } from "../../../src"; + +describe("grpc", () => { + it("getEndpoint should remove http and https from endpoint", () => { + const testCases = [ + { host: "http://localhost", port: "5000", expected: "localhost:5000" }, + { host: "https://localhost", port: "5000", expected: "localhost:5000" }, + { host: "localhost", port: "5000", expected: "localhost:5000" }, + ]; + + testCases.forEach((testCase) => { + expect(GRPCClient.getEndpoint(testCase.host, testCase.port)).toBe(testCase.expected); + }); + }); +}); diff --git a/test/unit/utils/Client.util.test.ts b/test/unit/utils/Client.util.test.ts index a1f00970..480344c1 100644 --- a/test/unit/utils/Client.util.test.ts +++ b/test/unit/utils/Client.util.test.ts @@ -20,6 +20,7 @@ import { getBulkPublishResponse, getClientOptions, createHTTPQueryParam, + parseEndpoint, } from "../../../src/utils/Client.util"; import { Map } from "google-protobuf"; import { PubSubBulkPublishEntry } from "../../../src/types/pubsub/PubSubBulkPublishEntry.type"; @@ -378,4 +379,385 @@ describe("Client.util", () => { expect(options).toEqual(expectedOptions); }); }); + + describe("parseEndpoint", () => { + const testCases = [ + { endpoint: ":5000", scheme: "http", host: "localhost", port: "5000" }, + { + endpoint: ":5000/v1/dapr", + scheme: "http", + host: "localhost", + port: "5000", + }, + + { endpoint: "localhost", scheme: "http", host: "localhost", port: "80" }, + { + endpoint: "localhost/v1/dapr", + scheme: "http", + host: "localhost", + port: "80", + }, + { + endpoint: "localhost:5000", + scheme: "http", + host: "localhost", + port: "5000", + }, + { + endpoint: "localhost:5000/v1/dapr", + scheme: "http", + host: "localhost", + port: "5000", + }, + + { + endpoint: "http://localhost", + scheme: "http", + host: "localhost", + port: "80", + }, + { + endpoint: "http://localhost/v1/dapr", + scheme: "http", + host: "localhost", + port: "80", + }, + { + endpoint: "http://localhost:5000", + scheme: "http", + host: "localhost", + port: "5000", + }, + { + endpoint: "http://localhost:5000/v1/dapr", + scheme: "http", + host: "localhost", + port: "5000", + }, + + { + endpoint: "https://localhost", + scheme: "https", + host: "localhost", + port: "443", + }, + { + endpoint: "https://localhost/v1/dapr", + scheme: "https", + host: "localhost", + port: "443", + }, + { + endpoint: "https://localhost:5000", + scheme: "https", + host: "localhost", + port: "5000", + }, + { + endpoint: "https://localhost:5000/v1/dapr", + scheme: "https", + host: "localhost", + port: "5000", + }, + + { endpoint: "127.0.0.1", scheme: "http", host: "127.0.0.1", port: "80" }, + { + endpoint: "127.0.0.1/v1/dapr", + scheme: "http", + host: "127.0.0.1", + port: "80", + }, + { + endpoint: "127.0.0.1:5000", + scheme: "http", + host: "127.0.0.1", + port: "5000", + }, + { + endpoint: "127.0.0.1:5000/v1/dapr", + scheme: "http", + host: "127.0.0.1", + port: "5000", + }, + + { + endpoint: "http://127.0.0.1", + scheme: "http", + host: "127.0.0.1", + port: "80", + }, + { + endpoint: "http://127.0.0.1/v1/dapr", + scheme: "http", + host: "127.0.0.1", + port: "80", + }, + { + endpoint: "http://127.0.0.1:5000", + scheme: "http", + host: "127.0.0.1", + port: "5000", + }, + { + endpoint: "http://127.0.0.1:5000/v1/dapr", + scheme: "http", + host: "127.0.0.1", + port: "5000", + }, + + { + endpoint: "https://127.0.0.1", + scheme: "https", + host: "127.0.0.1", + port: "443", + }, + { + endpoint: "https://127.0.0.1/v1/dapr", + scheme: "https", + host: "127.0.0.1", + port: "443", + }, + { + endpoint: "https://127.0.0.1:5000", + scheme: "https", + host: "127.0.0.1", + port: "5000", + }, + { + endpoint: "https://127.0.0.1:5000/v1/dapr", + scheme: "https", + host: "127.0.0.1", + port: "5000", + }, + + { + endpoint: "[2001:db8:1f70:0:999:de8:7648:6e8]", + scheme: "http", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "80", + }, + { + endpoint: "[2001:db8:1f70:0:999:de8:7648:6e8]/v1/dapr", + scheme: "http", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "80", + }, + { + endpoint: "[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + scheme: "http", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "5000", + }, + { + endpoint: "[2001:db8:1f70:0:999:de8:7648:6e8]:5000/v1/dapr", + scheme: "http", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "5000", + }, + + { + endpoint: "http://[2001:db8:1f70:0:999:de8:7648:6e8]", + scheme: "http", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "80", + }, + { + endpoint: "http://[2001:db8:1f70:0:999:de8:7648:6e8]/v1/dapr", + scheme: "http", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "80", + }, + { + endpoint: "http://[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + scheme: "http", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "5000", + }, + { + endpoint: "http://[2001:db8:1f70:0:999:de8:7648:6e8]:5000/v1/dapr", + scheme: "http", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "5000", + }, + + { + endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]", + scheme: "https", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "443", + }, + { + endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]/v1/dapr", + scheme: "https", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "443", + }, + { + endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]:5000", + scheme: "https", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "5000", + }, + { + endpoint: "https://[2001:db8:1f70:0:999:de8:7648:6e8]:5000/v1/dapr", + scheme: "https", + host: "2001:db8:1f70:0:999:de8:7648:6e8", + port: "5000", + }, + + { endpoint: "domain.com", scheme: "http", host: "domain.com", port: "80" }, + { + endpoint: "domain.com/v1/grpc", + scheme: "http", + host: "domain.com", + port: "80", + }, + { + endpoint: "domain.com:5000", + scheme: "http", + host: "domain.com", + port: "5000", + }, + { + endpoint: "domain.com:5000/v1/dapr", + scheme: "http", + host: "domain.com", + port: "5000", + }, + + { + endpoint: "http://domain.com", + scheme: "http", + host: "domain.com", + port: "80", + }, + { + endpoint: "http://domain.com/v1/dapr", + scheme: "http", + host: "domain.com", + port: "80", + }, + { + endpoint: "http://domain.com:5000", + scheme: "http", + host: "domain.com", + port: "5000", + }, + { + endpoint: "http://domain.com:5000/v1/dapr", + scheme: "http", + host: "domain.com", + port: "5000", + }, + + { + endpoint: "https://domain.com", + scheme: "https", + host: "domain.com", + port: "443", + }, + { + endpoint: "https://domain.com/v1/dapr", + scheme: "https", + host: "domain.com", + port: "443", + }, + { + endpoint: "https://domain.com:5000", + scheme: "https", + host: "domain.com", + port: "5000", + }, + { + endpoint: "https://domain.com:5000/v1/dapr", + scheme: "https", + host: "domain.com", + port: "5000", + }, + + { + endpoint: "abc.domain.com", + scheme: "http", + host: "abc.domain.com", + port: "80", + }, + { + endpoint: "abc.domain.com/v1/grpc", + scheme: "http", + host: "abc.domain.com", + port: "80", + }, + { + endpoint: "abc.domain.com:5000", + scheme: "http", + host: "abc.domain.com", + port: "5000", + }, + { + endpoint: "abc.domain.com:5000/v1/dapr", + scheme: "http", + host: "abc.domain.com", + port: "5000", + }, + + { + endpoint: "http://abc.domain.com/v1/dapr", + scheme: "http", + host: "abc.domain.com", + port: "80", + }, + { + endpoint: "http://abc.domain.com/v1/dapr", + scheme: "http", + host: "abc.domain.com", + port: "80", + }, + { + endpoint: "http://abc.domain.com:5000/v1/dapr", + scheme: "http", + host: "abc.domain.com", + port: "5000", + }, + { + endpoint: "http://abc.domain.com:5000/v1/dapr/v1/dapr", + scheme: "http", + host: "abc.domain.com", + port: "5000", + }, + + { + endpoint: "https://abc.domain.com/v1/dapr", + scheme: "https", + host: "abc.domain.com", + port: "443", + }, + { + endpoint: "https://abc.domain.com/v1/dapr", + scheme: "https", + host: "abc.domain.com", + port: "443", + }, + { + endpoint: "https://abc.domain.com:5000/v1/dapr", + scheme: "https", + host: "abc.domain.com", + port: "5000", + }, + { + endpoint: "https://abc.domain.com:5000/v1/dapr/v1/dapr", + scheme: "https", + host: "abc.domain.com", + port: "5000", + }, + ]; + + testCases.forEach(({ endpoint, scheme, host, port }) => { + it(`should correctly parse ${endpoint}`, () => { + const result = parseEndpoint(endpoint); + expect(result[0]).toEqual(scheme); + expect(result[1]).toEqual(host); + expect(result[2]).toEqual(port); + }); + }); + }); });