diff --git a/packages/durabletask-js/src/client/client.ts b/packages/durabletask-js/src/client/client.ts index 62a7b346..af90fc7c 100644 --- a/packages/durabletask-js/src/client/client.ts +++ b/packages/durabletask-js/src/client/client.ts @@ -998,6 +998,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()); @@ -1051,6 +1059,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 910fec60..def880a4 100644 --- a/packages/durabletask-js/test/entity-client.spec.ts +++ b/packages/durabletask-js/test/entity-client.spec.ts @@ -396,6 +396,65 @@ describe("EntityInstanceId.fromString", () => { }); }); +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.", + ); + }); + }); +}); + describe("getEntities query normalization", () => { const { TaskHubGrpcClient } = require("../src");