import { describe, it, expect } from "vitest"; import { PendingRequestMap, buildCallHandler } from "../src/call.js"; import { CallError, InfrastructureErrorCode } from "../src/error.js"; import { OperationRegistry } from "../src/registry.js"; import { Type } from "@alkdev/typebox"; import { OperationType } from "../src/types.js"; import type { Identity } from "../src/types.js"; import { localEnvelope, isResponseEnvelope, type ResponseEnvelope } from "../src/response-envelope.js"; describe("PendingRequestMap", () => { it("creates instance without event target", () => { const map = new PendingRequestMap(); expect(map.getPendingCount()).toBe(0); }); it("creates instance with event target", () => { const target = new EventTarget(); const map = new PendingRequestMap(target); expect(map.getPendingCount()).toBe(0); }); it("call() resolves when respond() is called with envelope", async () => { const map = new PendingRequestMap(); const callPromise = map.call("test.op", { value: "hello" }); setTimeout(() => { const requestId = [...map["requests"].keys()][0]; map.respond(requestId, localEnvelope({ result: "world" }, "test.op")); }, 10); const result = await callPromise; expect(isResponseEnvelope(result)).toBe(true); expect(result.meta.source).toBe("local"); expect(result.data).toEqual({ result: "world" }); }); it("respond() throws when called with non-envelope value", () => { const map = new PendingRequestMap(); expect(() => map.respond("req-1", { result: "world" } as any)).toThrow("ResponseEnvelope"); }); it("call() rejects when emitError() is called", async () => { const map = new PendingRequestMap(); const callPromise = map.call("test.op", { value: "hello" }); setTimeout(() => { const requestId = [...map["requests"].keys()][0]; map.emitError(requestId, "CUSTOM_ERROR", "Something went wrong"); }, 10); await expect(callPromise).rejects.toThrow("Something went wrong"); await expect(callPromise).rejects.toBeInstanceOf(CallError); }); it("abort() rejects the pending call", async () => { const map = new PendingRequestMap(); const callPromise = map.call("test.op", { value: "hello" }); setTimeout(() => { const requestId = [...map["requests"].keys()][0]; map.abort(requestId); }, 10); await expect(callPromise).rejects.toThrow("was aborted"); await expect(callPromise).rejects.toBeInstanceOf(CallError); }); it("call() with deadline times out", async () => { const map = new PendingRequestMap(); const deadline = Date.now() + 50; const callPromise = map.call("test.op", { value: "hello" }, { deadline }); await expect(callPromise).rejects.toThrow("timed out"); await expect(callPromise).rejects.toBeInstanceOf(CallError); }); it("tracks pending requests", () => { const map = new PendingRequestMap(); map.call("test.op1", {}); map.call("test.op2", {}); expect(map.getPendingCount()).toBe(2); }); it("cleans up after call resolves", async () => { const map = new PendingRequestMap(); const callPromise = map.call("test.op", { value: "hello" }); expect(map.getPendingCount()).toBe(1); const requestId = [...map["requests"].keys()][0]; map.respond(requestId, localEnvelope({ result: "done" }, "test.op")); await callPromise; expect(map.getPendingCount()).toBe(0); }); }); describe("checkAccess resource access control", () => { function makeRegistry(accessControlOverrides: Record = {}) { const registry = new OperationRegistry(); registry.register({ name: "guarded", namespace: "test", version: "1.0.0", type: OperationType.QUERY, description: "guarded op", inputSchema: Type.Object({}), outputSchema: Type.Object({ ok: Type.Boolean() }), accessControl: { requiredScopes: [], resourceType: "project", resourceAction: "read", ...accessControlOverrides, }, handler: async () => ({ ok: true }), }); registry.register({ name: "open", namespace: "test", version: "1.0.0", type: OperationType.QUERY, description: "open op", inputSchema: Type.Object({}), outputSchema: Type.Object({ ok: Type.Boolean() }), accessControl: { requiredScopes: [], }, handler: async () => ({ ok: true }), }); return registry; } it("denies access when resourceType/resourceAction are set and identity.resources is undefined", async () => { const registry = makeRegistry(); const handler = buildCallHandler({ registry }); const identity: Identity = { id: "user1", scopes: [] }; await expect( handler({ requestId: "r1", operationId: "test.guarded", input: {}, identity, }), ).rejects.toThrow("Access denied"); }); it("denies access when resourceType/resourceAction are set and identity.resources is empty", async () => { const registry = makeRegistry(); const handler = buildCallHandler({ registry }); const identity: Identity = { id: "user1", scopes: [], resources: {} }; await expect( handler({ requestId: "r1", operationId: "test.guarded", input: {}, identity, }), ).rejects.toThrow("Access denied"); }); it("denies access when identity.resources has no matching resource type", async () => { const registry = makeRegistry(); const handler = buildCallHandler({ registry }); const identity: Identity = { id: "user1", scopes: [], resources: { "document:abc": ["read"] }, }; await expect( handler({ requestId: "r1", operationId: "test.guarded", input: {}, identity, }), ).rejects.toThrow("Access denied"); }); it("denies access when identity.resources has matching type but wrong action", async () => { const registry = makeRegistry(); const handler = buildCallHandler({ registry }); const identity: Identity = { id: "user1", scopes: [], resources: { "project:abc": ["write"] }, }; await expect( handler({ requestId: "r1", operationId: "test.guarded", input: {}, identity, }), ).rejects.toThrow("Access denied"); }); it("grants access when identity.resources has matching type and action", async () => { const registry = makeRegistry(); const handler = buildCallHandler({ registry }); const identity: Identity = { id: "user1", scopes: [], resources: { "project:abc": ["read"] }, }; await expect( handler({ requestId: "r1", operationId: "test.guarded", input: {}, identity, }), ).resolves.toBeUndefined(); }); it("grants access when neither resourceType nor resourceAction are set", async () => { const registry = makeRegistry(); const handler = buildCallHandler({ registry }); const identity: Identity = { id: "user1", scopes: [] }; await expect( handler({ requestId: "r1", operationId: "test.open", input: {}, identity, }), ).resolves.toBeUndefined(); }); it("grants access when identity.resources matches and identity has no scopes required", async () => { const registry = makeRegistry(); const handler = buildCallHandler({ registry }); const identity: Identity = { id: "user1", scopes: ["some:scope"], resources: { "project:xyz": ["read", "write"] }, }; await expect( handler({ requestId: "r1", operationId: "test.guarded", input: {}, identity, }), ).resolves.toBeUndefined(); }); });