diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index d0be02e7..8055fbd7 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -994,6 +994,14 @@ export class TaskHubGrpcClient { input?: unknown, options?: SignalEntityOptions, ): Promise { + if (!id) { + throw new Error("signalEntity: 'id' is required."); + } + + if (!operationName) { + throw new Error("signalEntity: 'operationName' is required and cannot be empty."); + } + const req = new pb.SignalEntityRequest(); req.setInstanceid(id.toString()); req.setRequestid(randomUUID()); @@ -1047,6 +1055,10 @@ export class TaskHubGrpcClient { id: EntityInstanceId, includeState: boolean = true, ): Promise | undefined> { + if (!id) { + throw new Error("getEntity: 'id' is required."); + } + const req = new pb.GetEntityRequest(); req.setInstanceid(id.toString()); req.setIncludestate(includeState); diff --git a/packages/durabletask-js/test/entity-client.spec.ts b/packages/durabletask-js/test/entity-client.spec.ts index e77b46af..b6e063d6 100644 --- a/packages/durabletask-js/test/entity-client.spec.ts +++ b/packages/durabletask-js/test/entity-client.spec.ts @@ -351,3 +351,62 @@ describe("EntityInstanceId.fromString", () => { expect(() => EntityInstanceId.fromString("@onlyname")).toThrow(); }); }); + +describe("Entity Client Input Validation", () => { + // Validation guards throw before any gRPC call, so no connection is needed. + const { TaskHubGrpcClient } = require("../src"); + let client: InstanceType; + + beforeEach(() => { + client = new TaskHubGrpcClient({ hostAddress: "localhost:4001" }); + }); + + describe("signalEntity", () => { + it("should throw when id is null", async () => { + await expect(client.signalEntity(null as any, "op")).rejects.toThrow( + "signalEntity: 'id' is required.", + ); + }); + + it("should throw when id is undefined", async () => { + await expect(client.signalEntity(undefined as any, "op")).rejects.toThrow( + "signalEntity: 'id' is required.", + ); + }); + + it("should throw when operationName is empty string", async () => { + const entityId = new EntityInstanceId("counter", "my-counter"); + await expect(client.signalEntity(entityId, "")).rejects.toThrow( + "signalEntity: 'operationName' is required and cannot be empty.", + ); + }); + + it("should throw when operationName is null", async () => { + const entityId = new EntityInstanceId("counter", "my-counter"); + await expect(client.signalEntity(entityId, null as any)).rejects.toThrow( + "signalEntity: 'operationName' is required and cannot be empty.", + ); + }); + + it("should throw when operationName is undefined", async () => { + const entityId = new EntityInstanceId("counter", "my-counter"); + await expect(client.signalEntity(entityId, undefined as any)).rejects.toThrow( + "signalEntity: 'operationName' is required and cannot be empty.", + ); + }); + }); + + describe("getEntity", () => { + it("should throw when id is null", async () => { + await expect(client.getEntity(null as any)).rejects.toThrow( + "getEntity: 'id' is required.", + ); + }); + + it("should throw when id is undefined", async () => { + await expect(client.getEntity(undefined as any)).rejects.toThrow( + "getEntity: 'id' is required.", + ); + }); + }); +});