From 795088c5196c7cb740f8a7c3cdaf0ca8aab469a2 Mon Sep 17 00:00:00 2001 From: 117 <16513382+117@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:00:08 -0600 Subject: [PATCH] Update dependencies and add new tests for createClient function --- factory/createClient.test.ts | 178 +++++++++++++++++++++++------------ factory/createClient.ts | 21 +++-- utils/mockFetch.ts | 8 ++ 3 files changed, 136 insertions(+), 71 deletions(-) create mode 100644 utils/mockFetch.ts diff --git a/factory/createClient.test.ts b/factory/createClient.test.ts index 86be539..5f22c99 100644 --- a/factory/createClient.test.ts +++ b/factory/createClient.test.ts @@ -1,70 +1,124 @@ -import { assert } from "https://deno.land/std@0.220.0/assert/mod.ts"; -import { websocket } from "../api/marketData.ts"; +import { assert } from "https://deno.land/std@0.217.0/assert/assert.ts"; +import { assertThrows } from "https://deno.land/std@0.220.0/assert/assert_throws.ts"; +import { mockFetch } from "../utils/mockFetch.ts"; import { createClient } from "./createClient.ts"; -Deno.test("creates a client with trade and marketData APIs", () => { - const { trade, marketData } = createClient({ - keyId: "test", - secretKey: "test", +Deno.test("should create a trade client with valid options", () => { + const client = createClient({ baseURL: "https://paper-api.alpaca.markets", + keyId: "EXAMPLE_KEY_ID", + secretKey: "EXAMPLE_KEY_SECRET", }); - assert(trade !== undefined, "trade"); - assert(websocket !== undefined, "marketData"); - assert( - typeof trade.rest.v2.account.get === "function", - "trade.v2.account.get" - ); - assert( - typeof marketData.rest.v2.stocks.getConditionCodes === "function", - "marketData.v2.stocks.getConditionCodes" + assert(client.account !== undefined); + assert(client.orders.create !== undefined); +}); + +Deno.test("should create a market data client with valid options", () => { + const client = createClient({ + baseURL: "https://data.alpaca.markets", + keyId: "EXAMPLE_KEY_ID", + secretKey: "EXAMPLE_KEY_SECRET", + }); + + assert(client.rest.v1beta1 !== undefined); + assert(client.rest.v1beta3 !== undefined); +}); + +Deno.test("should throw an error with an invalid base URL", () => { + assertThrows( + () => { + createClient({ + // deno-lint-ignore ban-ts-comment + // @ts-expect-error + baseURL: "https://invalid-url.com", + keyId: "EXAMPLE_KEY_ID", + secretKey: "EXAMPLE_KEY_SECRET", + }); + }, + Error, + "invalid base URL" ); }); -// Deno.test("createClient - request handles parameters and data", async () => { -// const client = createClient({ -// keyId: "testKeyId", -// secretKey: "testSecretKey", -// baseURL: "https://example.com", -// }); - -// const response = await client.rest.trade({ -// path: "/test/{param}", -// params: { param: "value" }, -// data: { test: "data" }, -// }); - -// assertEquals(response.finalPath, "/test/value"); -// assertEquals(response.data, { test: "data" }); -// }); - -// Deno.test("createClient - throws error on API call failure", async () => { -// const client = createClient({ -// keyId: "testKeyId", -// secretKey: "testSecretKey", -// baseURL: "https://example.com", -// }); - -// await assertRejects( -// () => -// client.rest.trade({ -// path: "/invalid-endpoint", -// }), -// Error, -// "API call failed" -// ); -// }); - -// Deno.test("createClient - constructs websocket clients correctly", () => { -// const { websocket } = createClient({ -// keyId: "PK1OHDJBZQ6J5HQJZBXX", -// secretKey: "7ntdrZayazQkRxINbLWcn4ib0Nv58AlTQH0IqzbQ", -// baseURL: "https://paper-api.alpaca.markets", -// }); - -// assert(websocket.trade instanceof TradeWebSocket); -// assert(websocket.marketData.stock instanceof StockDataWebSocket); -// assert(websocket.marketData.crypto instanceof CryptoWebSocket); -// assert(websocket.marketData.news instanceof NewsWebSocket); -// assert(websocket.marketData.options instanceof OptionsWebSocket); -// }); +Deno.test("should use the provided token bucket options", () => { + const tokenBucketOptions = { + capacity: 100, + fillRate: 2, + }; + + const client = createClient({ + baseURL: "https://paper-api.alpaca.markets", + keyId: "EXAMPLE_KEY_ID", + secretKey: "EXAMPLE_KEY_SECRET", + tokenBucket: tokenBucketOptions, + }); + + assert(client._context.options.tokenBucket === tokenBucketOptions); +}); + +Deno.test("should use default token bucket options if not provided", () => { + const client = createClient({ + baseURL: "https://paper-api.alpaca.markets", + keyId: "EXAMPLE_KEY_ID", + secretKey: "EXAMPLE_KEY_SECRET", + }); + + assert(client._context.options.tokenBucket === undefined); +}); + +Deno.test("should make a request with the correct options", async () => { + const mockResponse = { mock: "data" }; + const originalFetch = globalThis.fetch; + // deno-lint-ignore ban-ts-comment + // @ts-expect-error + globalThis.fetch = mockFetch(mockResponse); + + const client = createClient({ + baseURL: "https://paper-api.alpaca.markets", + keyId: "EXAMPLE_KEY_ID", + secretKey: "EXAMPLE_KEY_SECRET", + }); + + const response = await client._context.request({ + path: "/v2/account", + }); + + assert(response === mockResponse); + + globalThis.fetch = originalFetch; +}); + +Deno.test("should throttle requests based on token bucket", async () => { + const mockResponse = { mock: "data" }; + const originalFetch = globalThis.fetch; + + // deno-lint-ignore ban-ts-comment + // @ts-expect-error + globalThis.fetch = mockFetch(mockResponse); + + const client = createClient({ + baseURL: "https://paper-api.alpaca.markets", + keyId: "EXAMPLE_KEY_ID", + secretKey: "EXAMPLE_KEY_SECRET", + tokenBucket: { + capacity: 2, + fillRate: 1, + }, + }); + + const startTime = Date.now(); + + await Promise.all([ + client._context.request({ path: "/v2/account" }), + client._context.request({ path: "/v2/account" }), + client._context.request({ path: "/v2/account" }), + ]); + + const endTime = Date.now(); + const elapsedTime = endTime - startTime; + + assert(elapsedTime >= 2000, "Requests should be throttled"); + + globalThis.fetch = originalFetch; +}); diff --git a/factory/createClient.ts b/factory/createClient.ts index a147f78..8f03d88 100644 --- a/factory/createClient.ts +++ b/factory/createClient.ts @@ -1,10 +1,8 @@ import marketData from "../api/marketData.ts"; import trade from "../api/trade.ts"; - import { TokenBucketOptions, createTokenBucket } from "./createTokenBucket.ts"; export type Trade = ReturnType; - export type MarketData = ReturnType; // Infer the client type based on the base URL @@ -34,9 +32,14 @@ export type ClientContext = { request: (options: RequestOptions) => Promise; }; +export type ClientWithContext = + ClientFactoryMap[T] & { + _context: ClientContext; + }; + export function createClient( options: CreateClientOptions & { baseURL: T } -): ClientFactoryMap[T] { +): ClientWithContext { // Create a token bucket for rate limiting const bucket = createTokenBucket(options.tokenBucket); @@ -60,7 +63,6 @@ export function createClient( // Hold the final path let modified = path; - if (params) { // Replace path parameters with actual values for (const [key, value] of Object.entries(params)) { @@ -89,7 +91,6 @@ export function createClient( `Failed to ${method} ${url}: ${response.status} ${response.statusText}` ); } - return response.json(); }); }; @@ -100,15 +101,17 @@ export function createClient( request, }; - // Conditonally return client based on the base URL - const factory = (context: ClientContext): ClientFactoryMap[T] => { + // Conditionally return client based on the base URL + const factory = (context: ClientContext): ClientWithContext => { + let client: ClientFactoryMap[T]; if (options.baseURL === "https://paper-api.alpaca.markets") { - return trade(context) as ClientFactoryMap[T]; + client = trade(context) as ClientFactoryMap[T]; } else if (options.baseURL === "https://data.alpaca.markets") { - return marketData(context) as ClientFactoryMap[T]; + client = marketData(context) as ClientFactoryMap[T]; } else { throw new Error("invalid base URL"); } + return Object.assign(client, { _context: context }); }; return factory(context); diff --git a/utils/mockFetch.ts b/utils/mockFetch.ts new file mode 100644 index 0000000..07a059e --- /dev/null +++ b/utils/mockFetch.ts @@ -0,0 +1,8 @@ +export const mockFetch = + // deno-lint-ignore no-explicit-any + (response: any) => (_url: string, _init?: RequestInit) => { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(response), + } as Response); + };