Skip to content

http-utils.test

Tests for http utils.test functionality in the Artinet SDK.

Test Suites

  • HTTP Utils
  • sendJsonRpcRequest
  • sendGetRequest
  • handleJsonRpcResponse
  • handleJsonResponse
  • parseResponse
  • executeJsonRpcRequest
  • executeGetRequest
  • handleEventStream
  • executeStreamEvents
  • createJsonRpcRequest
  • sendJsonRpcRequest with network errors
  • sendGetRequest with network errors
  • parseResponse additional cases
  • handleJsonResponse additional cases

Source Code

import { jest } from "@jest/globals";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import {
  sendJsonRpcRequest,
  sendGetRequest,
  handleJsonRpcResponse,
  handleJsonResponse,
  executeJsonRpcRequest,
  executeGetRequest,
  createJsonRpcRequest,
  parseResponse,
  handleEventStream,
  ErrorCodeParseError,
  JSONRPCRequest,
  JSONRPCResponse,
  SystemError,
  configureLogger,
} from "../src/index.js";

// Define a TestRequest type that matches JSONRPCRequest constraint
type TestMethod =
  | "test/method"
  | "test/empty"
  | "test/invalid"
  | "test/error"
  | "test/network-error"
  | "test/streaming";

// Define a custom request type for testing
interface TestRequest extends JSONRPCRequest {
  method: TestMethod;
  params: {
    param?: string;
    status?: string;
    [key: string]: any;
  };
}

configureLogger({ level: "silent" });

// Setup MSW server for mocking HTTP requests
const server = setupServer(
  // Mock successful request
  http.post("https://example.com/api", ({ request }) => {
    const url = new URL(request.url);
    if (url.pathname === "/api") {
      const contentType = request.headers.get("Content-Type") || "";
      if (contentType.includes("application/json")) {
        return HttpResponse.json({
          jsonrpc: "2.0",
          id: "test-id",
          result: { foo: "bar" },
        });
      }
    }
    return HttpResponse.json({}, { status: 404 });
  }),

  // Mock successful GET request
  http.get("https://example.com/api", () => {
    return HttpResponse.json({ success: true });
  }),

  // Mock network error GET request
  http.get("https://example.com/api/error", () => {
    return new HttpResponse(null, { status: 500 });
  }),

  // Mock invalid JSON response
  http.get("https://example.com/api/invalid", () => {
    return new HttpResponse("not a json", {
      status: 200,
      headers: { "Content-Type": "text/plain" },
    });
  }),

  // Mock invalid JSON-RPC response with null result
  http.get("https://example.com/api/null-result", () => {
    return HttpResponse.json({
      jsonrpc: "2.0",
      id: "test-id",
    });
  }),

  // Mock empty response
  http.get("https://example.com/api/empty-response", () => {
    return new HttpResponse(null, { status: 204 });
  }),

  // Mock network error for specific endpoints
  http.get("https://example.com/api/network-error", () => {
    throw new Error("Network error");
  }),

  http.post("https://example.com/api/network-error", () => {
    throw new Error("Network error");
  }),

  // Mock RPC error response
  http.post("https://example.com/api/rpc-error", () => {
    return HttpResponse.json(
      {
        jsonrpc: "2.0",
        id: "test-id",
        error: {
          code: -32603,
          message: "Internal error",
          data: { details: "Something went wrong" },
        },
      },
      { status: 200 }
    );
  }),

  // Mock invalid streaming response
  http.post("https://example.com/api/streaming", () => {
    const encoder = new TextEncoder();
    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue(
          encoder.encode(
            `event: message\ndata: ${JSON.stringify({
              jsonrpc: "2.0",
              id: "test-id",
              result: { status: "working" },
            })}\n\n`
          )
        );
        controller.enqueue(
          encoder.encode(
            `event: message\ndata: ${JSON.stringify({
              jsonrpc: "2.0",
              id: "test-id",
              result: { status: "completed" },
            })}\n\n`
          )
        );
        controller.close();
      },
    });

    return new HttpResponse(stream, {
      headers: {
        "Content-Type": "text/event-stream",
        Connection: "keep-alive",
        "Cache-Control": "no-cache",
      },
    });
  }),

  http.post("https://example.com/api/invalid-streaming", () => {
    const encoder = new TextEncoder();
    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue(encoder.encode(`event: message\ndata: invalid\n\n`));
        controller.close();
      },
    });

    return new HttpResponse(stream, {
      headers: {
        "Content-Type": "text/event-stream",
        Connection: "keep-alive",
        "Cache-Control": "no-cache",
      },
    });
  })
);

describe("HTTP Utils", () => {
  beforeAll(() => {
    server.listen();
  });

  afterAll(() => {
    server.close();
  });

  beforeEach(() => {
    server.resetHandlers();
  });

  describe("sendJsonRpcRequest", () => {
    test("should send a JSON-RPC request and return the response", async () => {
      const response = await sendJsonRpcRequest(
        new URL("https://example.com/api"),
        "test/method" as any,
        {} as any, // Empty object to avoid type issues
        { "Custom-Header": "value" }
      );

      expect(response.ok).toBe(true);
      const data = await response.json();
      expect(data).toEqual({
        jsonrpc: "2.0",
        id: expect.any(String),
        result: { foo: "bar" },
      });
    });

    test("should handle network errors", async () => {
      // Configure MSW to simulate a network error for this specific test
      server.use(
        http.post("https://example.com/api", () => {
          return new HttpResponse(null, { status: 500 });
        })
      );

      try {
        await sendJsonRpcRequest(
          new URL("https://example.com/api"),
          "test/method" as any,
          {} as any
        );
        // If we get here, the test should fail only if we got a response
        // but we expected to throw
      } catch (error) {
        // Success - we expected it to throw
        expect(error).toBeDefined();
      }
    });
  });

  describe("sendGetRequest", () => {
    test("should send a GET request and return the response", async () => {
      const response = await sendGetRequest(
        new URL("https://example.com/api"),
        { "Custom-Header": "value" }
      );

      expect(response.ok).toBe(true);
      const data = await response.json();
      expect(data).toEqual({ success: true });
    });

    test("should handle network errors", async () => {
      // Configure MSW to simulate a network error for this specific test
      server.use(
        http.get("https://example.com/api", () => {
          return new HttpResponse(null, { status: 500 });
        })
      );

      try {
        await sendGetRequest(new URL("https://example.com/api"));
        // If we got here, the test should only fail if we got a response
        // when we expected it to throw
      } catch (error) {
        // Success - we expected it to throw
        expect(error).toBeDefined();
      }
    });
  });

  describe("handleJsonRpcResponse", () => {
    test("should parse and return the result from a JSON-RPC response", async () => {
      const response = await fetch("https://example.com/api", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          jsonrpc: "2.0",
          id: "test-id",
          method: "test/method",
        }),
      });

      const result = await handleJsonRpcResponse<
        JSONRPCResponse<{ foo: string }>
      >(response, "test/method");
      expect(result).toEqual({ foo: "bar" });
    });

    test("should handle HTTP errors with JSON-RPC errors", async () => {
      // Override the response for this specific test
      server.use(
        http.post("https://example.com/api", () => {
          return HttpResponse.json(
            {
              jsonrpc: "2.0",
              id: "test-id",
              error: {
                code: -32603,
                message: "Internal error",
              },
            },
            { status: 400 }
          );
        })
      );

      const response = await fetch("https://example.com/api", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          jsonrpc: "2.0",
          id: "test-id",
          method: "test/error",
        }),
      });

      try {
        await handleJsonRpcResponse<JSONRPCResponse>(response, "test/error");
        // If we get here, the test should fail
        expect(true).toBe(false); // Force test to fail
      } catch (error) {
        expect(error).toBeInstanceOf(SystemError);
      }
    });

    test("should handle HTTP errors with non-JSON-RPC responses", async () => {
      server.use(
        http.post("https://example.com/api", () => {
          return new HttpResponse("Not a JSON-RPC response", {
            status: 500,
            headers: { "Content-Type": "text/plain" },
          });
        })
      );

      const response = await fetch("https://example.com/api", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          jsonrpc: "2.0",
          id: "test-id",
          method: "test/method",
        }),
      });

      await expect(
        handleJsonRpcResponse<JSONRPCResponse>(response, "test/method")
      ).rejects.toThrow();
    });

    test("should handle responses with invalid JSON", async () => {
      server.use(
        http.post("https://example.com/api", () => {
          return new HttpResponse("not a json", {
            status: 200,
            headers: { "Content-Type": "text/plain" },
          });
        })
      );

      const response = await fetch("https://example.com/api", {
        method: "POST",
        body: JSON.stringify({
          jsonrpc: "2.0",
          id: "test-id",
          method: "test/method",
        }),
      });

      await expect(
        handleJsonRpcResponse<JSONRPCResponse>(response, "test/method")
      ).rejects.toThrow(SystemError);
    });

    test("should handle JSON-RPC errors with data field", async () => {
      const response = await fetch("https://example.com/api/rpc-error", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ jsonrpc: "2.0", id: "test-id" }),
      });

      await expect(
        handleJsonRpcResponse<JSONRPCResponse>(response, "test/error-with-data")
      ).rejects.toThrow(SystemError);
    });

    test("should handle non-A2AError exceptions during processing", async () => {
      // Use a direct test of the error handling path in handleJsonRpcResponse
      const invalidResponse = new Response(null, { status: 200 });

      try {
        await handleJsonRpcResponse<JSONRPCResponse>(
          invalidResponse,
          "test/error-handling"
        );
        // Should not reach here
        expect("this line").toBe("not reached");
      } catch (error) {
        // Verify it's an A2AError with the right code
        expect(error instanceof SystemError).toBe(true);
        expect((error as SystemError).code).toBe(ErrorCodeParseError);
      }
    });
  });

  describe("handleJsonResponse", () => {
    test("should parse and return JSON from a successful response", async () => {
      const response = await fetch("https://example.com/api");
      const result = await handleJsonResponse<{ success: boolean }>(
        response,
        "test-endpoint"
      );
      expect(result).toEqual({ success: true });
    });

    test("should handle HTTP errors", async () => {
      const response = await fetch("https://example.com/api/error");
      await expect(
        handleJsonResponse<any>(response, "error-endpoint")
      ).rejects.toThrow();
    });

    test("should handle invalid JSON", async () => {
      const response = await fetch("https://example.com/api/invalid");
      await expect(
        handleJsonResponse<any>(response, "invalid-endpoint")
      ).rejects.toThrow(SystemError);
    });

    test("should handle empty responses", async () => {
      const response = await fetch("https://example.com/api/empty-response");
      await expect(
        handleJsonResponse<any>(response, "empty-response-endpoint")
      ).rejects.toThrow(SystemError);
    });
  });

  describe("parseResponse", () => {
    test("should parse valid JSON-RPC response", () => {
      const data = JSON.stringify({
        jsonrpc: "2.0",
        id: "test-id",
        result: { foo: "bar" },
      });

      const result = parseResponse<JSONRPCResponse<{ foo: string }>>(data);
      expect(result).toEqual({
        jsonrpc: "2.0",
        id: "test-id",
        result: { foo: "bar" },
      });
    });

    test("should throw for empty data", () => {
      expect(() => parseResponse("")).toThrow(SystemError);
    });

    test("should throw for JSON-RPC errors", () => {
      const data = JSON.stringify({
        jsonrpc: "2.0",
        id: "test-id",
        error: {
          code: -32601,
          message: "Method not found",
        },
      });

      expect(() => parseResponse(data)).toThrow(SystemError);
    });

    test("should throw for JSON-RPC errors with data field", () => {
      const data = JSON.stringify({
        jsonrpc: "2.0",
        id: "test-id",
        error: {
          code: -32603,
          message: "Internal error",
          data: { details: "Something went wrong" },
        },
      });

      expect(() => parseResponse(data)).toThrow(SystemError);
    });

    test("should throw for invalid JSON-RPC structure", () => {
      const data = JSON.stringify({
        not: "jsonrpc",
      });

      expect(() => parseResponse(data)).toThrow(SystemError);
    });

    test("should throw for missing result", () => {
      const data = JSON.stringify({
        jsonrpc: "2.0",
        id: "test-id",
      });

      expect(() => parseResponse(data)).toThrow(SystemError);
    });

    test("should throw for invalid JSON", () => {
      expect(() => parseResponse("invalid json")).toThrow(SystemError);
    });
  });

  describe("executeJsonRpcRequest", () => {
    test("should execute a JSON-RPC request and return the result", async () => {
      const result = await executeJsonRpcRequest(
        new URL("https://example.com/api"),
        "test/method" as any,
        {} as any, // Empty object to avoid type issues
        { "Custom-Header": "value" }
      );

      expect(result).toEqual({ foo: "bar" });
    });

    test("should handle errors", async () => {
      // Override the response for this specific test
      server.use(
        http.post("https://example.com/api", () => {
          return HttpResponse.json(
            {
              jsonrpc: "2.0",
              id: "test-id",
              error: {
                code: -32603,
                message: "Internal error",
              },
            },
            { status: 400 }
          );
        })
      );

      try {
        await executeJsonRpcRequest(
          new URL("https://example.com/api"),
          "test/error" as any,
          {} as any
        );
        // If we got here, the test should only fail if we got a response
        // when we expected it to throw
      } catch (error) {
        // Success - we expected it to throw
        expect(error).toBeDefined();
      }
    });

    test("should handle network errors", async () => {
      server.use(
        http.post("https://example.com/api", () => {
          throw new Error("Network error");
        })
      );

      try {
        await executeJsonRpcRequest(
          new URL("https://example.com/api"),
          "test/method" as any,
          {} as any
        );
        // If we got here, the test should only fail if we got a response
        // when we expected it to throw
      } catch (error) {
        // Success - we expected it to throw
        expect(error).toBeDefined();
      }
    });

    test("should accept different accept headers", async () => {
      const result = await executeJsonRpcRequest(
        new URL("https://example.com/api"),
        "test/method" as any,
        {} as any, // Empty object to avoid type issues
        { "Custom-Header": "value" },
        "application/json"
      );

      expect(result).toEqual({ foo: "bar" });
    });
  });

  describe("executeGetRequest", () => {
    test("should execute a GET request and return the parsed result", async () => {
      const result = await executeGetRequest<{ success: boolean }>(
        new URL("https://example.com/api"),
        { "Custom-Header": "value" },
        "test-endpoint"
      );

      expect(result).toEqual({ success: true });
    });

    test("should handle errors", async () => {
      await expect(
        executeGetRequest<any>(
          new URL("https://example.com/api/error"),
          {},
          "error-endpoint"
        )
      ).rejects.toThrow(SystemError);
    });

    test("should handle network errors", async () => {
      server.use(
        http.get("https://example.com/api", () => {
          throw new Error("Network error");
        })
      );

      await expect(
        executeGetRequest<any>(
          new URL("https://example.com/api"),
          {},
          "network-error-endpoint"
        )
      ).rejects.toThrow(SystemError);
    });

    test("should work without optional parameters", async () => {
      const result = await executeGetRequest<{ success: boolean }>(
        new URL("https://example.com/api")
      );

      expect(result).toEqual({ success: true });
    });
  });

  describe("handleEventStream", () => {
    test("should handle empty events array", async () => {
      const encoder = new TextEncoder();
      const stream = new ReadableStream({
        start(controller) {
          // Send an event with valid structure but no data we care about
          controller.enqueue(
            encoder.encode(
              `event: message\ndata: ${JSON.stringify({
                jsonrpc: "2.0",
                id: "test-id",
                // Missing result property intentionally
              })}\n\n`
            )
          );
          controller.close();
        },
      });

      const response = new Response(stream, {
        headers: {
          "Content-Type": "text/event-stream",
        },
      });

      const generator = handleEventStream<JSONRPCResponse>(response);
      const results: any[] = [];

      for await (const event of generator) {
        results.push(event);
      }

      // Should not yield any events since the input had none
      expect(results.length).toBe(0);
    });

    test("should handle parser errors in event data", async () => {
      const encoder = new TextEncoder();
      const stream = new ReadableStream({
        start(controller) {
          // Send an invalid event (not proper JSON)
          controller.enqueue(
            encoder.encode(`event: message\ndata: invalid-json\n\n`)
          );
          controller.close();
        },
      });

      const response = new Response(stream, {
        headers: {
          "Content-Type": "text/event-stream",
        },
      });

      const generator = handleEventStream<JSONRPCResponse>(response);
      const results: any[] = [];

      for await (const event of generator) {
        results.push(event);
      }

      // Should not yield any events since the input was invalid
      expect(results.length).toBe(0);
    });

    test("should handle valid events with undefined result", async () => {
      const encoder = new TextEncoder();
      const stream = new ReadableStream({
        start(controller) {
          // Send an event with null result to trigger the warning branch
          controller.enqueue(
            encoder.encode(
              `event: message\ndata: ${JSON.stringify({
                jsonrpc: "2.0",
                id: "test-id",
                result: undefined,
              })}\n\n`
            )
          );
          controller.close();
        },
      });

      const response = new Response(stream, {
        headers: {
          "Content-Type": "text/event-stream",
        },
      });

      const generator = handleEventStream<JSONRPCResponse>(response);
      const results: any[] = [];

      for await (const event of generator) {
        results.push(event);
      }

      // Should not yield any events since result was undefined
      expect(results.length).toBe(0);
    });
  });

  describe("executeStreamEvents", () => {
    test.skip("should execute a streaming request and yield events", () => {});

    test.skip("should handle errors in streaming requests", () => {});
  });

  describe("createJsonRpcRequest", () => {
    test("should create a properly formatted JSON-RPC request with custom ID", () => {
      const request = createJsonRpcRequest(
        "test/method" as any,
        {} as any, // Empty object to avoid type issues
        "custom-id"
      );
      expect(request).toEqual({
        jsonrpc: "2.0",
        id: "custom-id",
        method: "test/method",
        params: {},
      });
    });

    test("should create a properly formatted JSON-RPC request with auto-generated ID", () => {
      const request = createJsonRpcRequest(
        "test/method" as any,
        {} as any // Empty object to avoid type issues
      );
      expect(request).toEqual({
        jsonrpc: "2.0",
        id: expect.any(String), // UUID is auto-generated
        method: "test/method",
        params: {},
      });
    });
  });

  describe("sendJsonRpcRequest with network errors", () => {
    test("should handle network error that's not an Error instance", async () => {
      try {
        await sendJsonRpcRequest(
          new URL("https://example.com/api"),
          "test/method" as any,
          {} as any // Empty params to avoid type issues
        );
      } catch (error) {
        expect(error).toBeInstanceOf(SystemError);
        expect(error.message).toContain("Network connection lost");
      }
    });
  });

  describe("sendGetRequest with network errors", () => {
    test("should handle network error that's not an Error instance", async () => {
      try {
        await sendGetRequest(new URL("https://example.com/api"));
      } catch (error) {
        expect(error).toBeInstanceOf(SystemError);
        expect(error.message).toContain("Connection timeout");
      }
    });
  });

  describe("parseResponse additional cases", () => {
    test("should handle error that's not an A2AError during parsing", () => {
      try {
        parseResponse("invalid json");
      } catch (error) {
        expect(error).toBeInstanceOf(SystemError);
        expect((error as SystemError).code).toBe(ErrorCodeParseError);
      }
    });
  });

  describe("handleJsonResponse additional cases", () => {
    test("should handle error that's not an Error instance", async () => {
      server.use(
        http.get("https://example.com/api", () => {
          return new HttpResponse("", {
            status: 200,
            headers: { "Content-Type": "application/json" },
          });
        })
      );

      const response = await fetch("https://example.com/api");
      try {
        await handleJsonResponse(response, "custom-endpoint");
      } catch (error) {
        expect(error).toBeInstanceOf(SystemError);
        expect(error.message).toContain("Invalid JSON payload");
      }
    });
  });
});

Running the Tests

To run these tests:

  1. Clone the Artinet SDK repository
  2. Install dependencies with npm install
  3. Run the tests with npm test or specifically with npx jest http-utils.test.ts

Coverage