UNPKG

laplace-api

Version:

Client library for Laplace API for the US stock market and BIST (Istanbul stock market) fundamental financial data.

669 lines (575 loc) 22.4 kB
import { Logger } from "winston"; import { LaplaceConfiguration } from "../utilities/configuration"; import "./client_test_suite"; import { BISTBidAskStreamData, BISTStockStreamData, LivePriceClient, OrderbookLiveData } from "../client/live-price"; import { LivePriceFeed } from "../client/live-price-web-socket"; describe("LivePrice", () => { let client: LivePriceClient; let config: LaplaceConfiguration; let logger: Logger; let activeConnections: any[] = []; let activeTimeouts: NodeJS.Timeout[] = []; const TEST_CONSTANTS = { JEST_TIMEOUT: 15000, MAIN_TIMEOUT: 10000, }; beforeAll(async () => { config = (global as any).testSuite.config as LaplaceConfiguration; logger = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), } as unknown as Logger; client = new LivePriceClient(config, logger); }); afterEach(async () => { // Clear all active timeouts for (const timeout of activeTimeouts) { clearTimeout(timeout); } activeTimeouts = []; // Clean up all active connections for (const connection of activeConnections) { try { connection.close(); } catch (error) { console.log("Error closing connection:", error); } } activeConnections = []; }); afterAll(async () => { // Final cleanup for (const timeout of activeTimeouts) { clearTimeout(timeout); } for (const connection of activeConnections) { try { connection.close(); } catch (error) { console.log("Error closing connection in afterAll:", error); } } }); describe("GetLivePriceForBIST", () => { it( "should receive BIST live price data", async () => { const symbols = ["AKBNK"]; let receivedData: BISTStockStreamData | null = null; let receivedError: Error | null = null; const lc = client.getLivePriceForBIST(symbols); activeConnections.push(lc); try { const receiveChan = lc.receive(); // Set a timeout to avoid hanging const timeoutPromise = new Promise<void>((_, reject) => { const timeout = setTimeout( () => reject(new Error("Timeout waiting for data")), TEST_CONSTANTS.MAIN_TIMEOUT ); activeTimeouts.push(timeout); }); const dataPromise = (async () => { try { for await (const data of receiveChan) { receivedData = data; break; // Get first data and exit } } catch (error) { console.log("Error in data stream:", error); } })(); await Promise.race([dataPromise, timeoutPromise]); if (receivedData) { const tempReceivedData = (receivedData as BISTStockStreamData).d; console.log("Received BIST data:", tempReceivedData); expect(tempReceivedData.s).toBeDefined(); expect(typeof tempReceivedData.s).toBe("string"); expect(typeof tempReceivedData.p).toBe("number"); expect(typeof tempReceivedData.ch).toBe("number"); expect(typeof tempReceivedData.d).toBe("number"); } else { console.log("Timeout waiting for BIST data"); } } catch (error) { receivedError = error as Error; console.log("Received error:", receivedError.message); } finally { lc.close(); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("GetLivePriceForUS", () => { it( "should receive US live price data", async () => { const symbols = ["AAPL"]; let receivedData: any = null; let receivedError: Error | null = null; const lc = client.getLivePriceForUS(symbols); activeConnections.push(lc); try { const receiveChan = lc.receive(); // Set a timeout to avoid hanging const timeoutPromise = new Promise<void>((_, reject) => { const timeout = setTimeout( () => reject(new Error("Timeout waiting for data")), TEST_CONSTANTS.MAIN_TIMEOUT ); activeTimeouts.push(timeout); }); const dataPromise = (async () => { try { for await (const data of receiveChan) { receivedData = data; break; // Get first data and exit } } catch (error) { console.log("Error in data stream:", error); } })(); await Promise.race([dataPromise, timeoutPromise]); if (receivedData) { console.log("Received US data:", receivedData); expect(receivedData.s).toBeDefined(); expect(typeof receivedData.s).toBe("string"); expect(typeof receivedData.p).toBe("number"); expect(typeof receivedData.d).toBe("number"); expect(typeof receivedData.pc).toBe("number"); expect(typeof receivedData.ac).toBe("number"); } else { console.log("Timeout waiting for US data"); } } catch (error) { receivedError = error as Error; console.log("Received error:", receivedError.message); } finally { lc.close(); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("LivePriceSubscribe", () => { it( "should handle subscription changes", async () => { const initialSymbols = ["AKBNK"]; const newSymbols = ["TUPRS", "ASELS"]; const receivedData: string[] = []; let switchOccurred = false; const lc = client.getLivePriceForBIST(initialSymbols); activeConnections.push(lc); try { const receiveChan = lc.receive(); // Start receiving data const dataPromise = (async () => { try { for await (const data of receiveChan) { receivedData.push(data.d.s); // Switch symbols after 5 seconds if (!switchOccurred && receivedData.length > 0) { const switchTimeout = setTimeout(async () => { try { await lc.subscribe(newSymbols); receivedData.push("SWITCH"); switchOccurred = true; // Close after another 5 seconds const closeTimeout = setTimeout(() => { lc.close(); }, 5000); activeTimeouts.push(closeTimeout); } catch (error) { console.error("Error switching symbols:", error); } }, 5000); activeTimeouts.push(switchTimeout); } } } catch (error) { console.log("Error in subscription test:", error); } })(); // Set overall timeout const timeoutPromise = new Promise<void>((_, reject) => { const timeout = setTimeout( () => reject(new Error("Test timeout")), TEST_CONSTANTS.JEST_TIMEOUT ); activeTimeouts.push(timeout); }); await Promise.race([dataPromise, timeoutPromise]); // Verify we received data expect(receivedData.length).toBeGreaterThan(0); const switchIndex = receivedData.indexOf("SWITCH"); if (switchIndex > 0) { const beforeSwitch = receivedData.slice(0, switchIndex); expect(beforeSwitch.some((symbol) => symbol === "AKBNK")).toBe( true ); } if (switchIndex >= 0 && switchIndex < receivedData.length - 1) { const afterSwitch = receivedData.slice(switchIndex + 1); expect(afterSwitch.some((symbol) => symbol === "TUPRS")).toBe(true); expect(afterSwitch.some((symbol) => symbol === "ASELS")).toBe(true); } } catch (error) { console.log("Test error:", error); } finally { lc.close(); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("LivePriceClose", () => { it( "should close connection properly", async () => { const symbols = ["AKBNK"]; const lc = client.getLivePriceForBIST(symbols); activeConnections.push(lc); try { // Close immediately lc.close(); // Try to receive data after close const receiveChan = lc.receive(); let receivedAfterClose = false; try { for await (const data of receiveChan) { receivedAfterClose = true; break; } } catch (error) { // Expected to throw after close console.log("Expected error after close:", error); } // Should not receive data after close expect(receivedAfterClose).toBe(false); } catch (error) { console.error("Close test error:", error); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("GetDelayedPriceForBIST", () => { it( "should receive BIST delayed price data", async () => { const symbols = ["AKBNK"]; let receivedData: BISTStockStreamData | null = null; let receivedError: Error | null = null; const lc = client.getDelayedPriceForBIST(symbols); activeConnections.push(lc); try { const receiveChan = lc.receive(); const timeoutPromise = new Promise<void>((_, reject) => { const timeout = setTimeout( () => reject(new Error("Timeout waiting for delayed data")), TEST_CONSTANTS.MAIN_TIMEOUT ); activeTimeouts.push(timeout); }); const dataPromise = (async () => { try { for await (const data of receiveChan) { receivedData = data; break; } } catch (error) { console.log("Error in delayed data stream:", error); } })(); await Promise.race([dataPromise, timeoutPromise]); if (receivedData != null) { const tempReceivedData = (receivedData as BISTStockStreamData).d; console.log("Received BIST delayed data:", tempReceivedData); expect(tempReceivedData.s).toBeDefined(); expect(typeof tempReceivedData.s).toBe("string"); expect(typeof tempReceivedData.p).toBe("number"); expect(typeof tempReceivedData.ch).toBe("number"); expect(typeof tempReceivedData.d).toBe("number"); } else { console.log("Timeout waiting for BIST delayed data"); } } catch (error) { receivedError = error as Error; console.log("Received delayed error:", receivedError.message); } finally { lc.close(); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("GetOrderbookForBIST", () => { it( "should receive BIST orderbook data", async () => { const symbols = ["AKBNK"]; let receivedData: OrderbookLiveData | null = null; let receivedError: Error | null = null; const lc = client.getOrderbookForBIST(symbols); activeConnections.push(lc); try { const receiveChan = lc.receive(); const timeoutPromise = new Promise<void>((_, reject) => { const timeout = setTimeout( () => reject(new Error("Timeout waiting for orderbook data")), TEST_CONSTANTS.MAIN_TIMEOUT ); activeTimeouts.push(timeout); }); const dataPromise = (async () => { try { for await (const data of receiveChan) { console.log(data); receivedData = data; break; } } catch (error) { console.log("Error in orderbook data stream:", error); } })(); await Promise.race([dataPromise, timeoutPromise]); if (receivedData != null) { const tempReceivedData = receivedData as OrderbookLiveData; console.log("Received BIST orderbook data:", tempReceivedData); expect(tempReceivedData.symbol).toBeDefined(); expect(typeof tempReceivedData.symbol).toBe("string"); if (tempReceivedData.updated != null) { expect(Array.isArray(tempReceivedData.updated)).toBe(true); if (tempReceivedData.updated.length > 0) { const firsData = tempReceivedData.updated[0]; console.log("updated first data:", firsData) expect(typeof firsData.level).toBe("number"); expect(typeof firsData.vol).toBe("number"); expect(typeof firsData.orders).toBe("number"); expect(typeof firsData.p).toBe("number"); expect(typeof firsData.side).toBe("string"); } } if (tempReceivedData.deleted != null) { expect(Array.isArray(tempReceivedData.deleted)).toBe(true); if (tempReceivedData.deleted.length > 0) { const firsData = tempReceivedData.deleted[0]; console.log("deleted first data:", firsData) expect(typeof firsData.level).toBe("number"); expect(typeof firsData.side).toBe("string"); } } } else { console.log("Timeout waiting for BIST orderbook data"); } } catch (error) { receivedError = error as Error; console.log("Received orderbook error:", receivedError.message); } finally { lc.close(); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("Client Methods", () => { it( "should work with client methods", async () => { const symbols = ["THYAO"]; let receivedData: any = null; const lc = client.getLivePriceForBIST(symbols); activeConnections.push(lc); try { const receiveChan = lc.receive(); const timeoutPromise = new Promise<void>((_, reject) => { const timeout = setTimeout( () => reject(new Error("Timeout")), TEST_CONSTANTS.MAIN_TIMEOUT ); activeTimeouts.push(timeout); }); const dataPromise = (async () => { try { for await (const data of receiveChan) { receivedData = data; break; } } catch (error) { console.log("Error in client methods test:", error); } })(); await Promise.race([dataPromise, timeoutPromise]); if (receivedData) { expect(receivedData.s).toBeDefined(); expect(typeof receivedData.s).toBe("string"); } } finally { lc.close(); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("GetClientWebsocketUrl", () => { it( "should return a websocket url string", async () => { const resp = await client.getClientWebsocketUrl("test-integration-user", [LivePriceFeed.LiveBist]); expect(typeof resp).toBe("string"); expect(resp.length).toBeGreaterThan(0); }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("GetWebsocketUsageForMonth", () => { it( "should return an array of usage data", async () => { const resp = await client.getWebsocketUsageForMonth(1, 2025, LivePriceFeed.LiveBist); expect(Array.isArray(resp)).toBe(true); if (resp.length > 0) { expect(typeof resp[0].externalUserID).toBe("string"); expect(typeof resp[0].uniqueDeviceCount).toBe("number"); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); describe("Mock Tests (Data Injection)", () => { let mockClient: LivePriceClient; let cli: { request: jest.Mock }; beforeEach(() => { cli = { request: jest.fn() }; const config = (global as any).testSuite.config as LaplaceConfiguration; const mockLogger: Logger = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), } as unknown as Logger; mockClient = new LivePriceClient(config, mockLogger, cli as any); }); describe("getClientWebsocketUrl", () => { test("calls correct endpoint, sends correct body, and returns url string", async () => { cli.request.mockResolvedValueOnce({ data: { url: "wss://example.com/ws" } }); const resp = await mockClient.getClientWebsocketUrl("user123", [LivePriceFeed.LiveBist, LivePriceFeed.LiveUs]); expect(cli.request).toHaveBeenCalledTimes(1); const call = cli.request.mock.calls[0][0]; expect(call.method).toBe("POST"); expect(call.url).toBe("/api/v2/ws/url"); expect(call.data).toEqual({ externalUserId: "user123", feeds: [LivePriceFeed.LiveBist, LivePriceFeed.LiveUs] }); expect(resp).toBe("wss://example.com/ws"); }); test("bubbles up request error", async () => { cli.request.mockRejectedValueOnce(new Error("Unauthorized")); await expect(mockClient.getClientWebsocketUrl("user123", [LivePriceFeed.LiveBist])).rejects.toThrow("Unauthorized"); expect(cli.request).toHaveBeenCalledTimes(1); }); }); describe("getWebsocketUsageForMonth", () => { test("calls correct endpoint with correct query params and returns usage data", async () => { const mockUsage = [ { externalUserID: "user123", firstConnectionTime: new Date("2024-01-15"), uniqueDeviceCount: 3 }, ]; cli.request.mockResolvedValueOnce({ data: mockUsage }); const resp = await mockClient.getWebsocketUsageForMonth(1, 2024, LivePriceFeed.LiveBist); expect(cli.request).toHaveBeenCalledTimes(1); const call = cli.request.mock.calls[0][0]; expect(call.method).toBe("GET"); expect(call.url).toBe("/api/v1/ws/report"); expect(call.params).toEqual({ month: 1, year: 2024, feedType: LivePriceFeed.LiveBist }); expect(resp).toEqual(mockUsage); }); test("bubbles up request error", async () => { cli.request.mockRejectedValueOnce(new Error("Forbidden")); await expect(mockClient.getWebsocketUsageForMonth(1, 2024, LivePriceFeed.LiveBist)).rejects.toThrow("Forbidden"); expect(cli.request).toHaveBeenCalledTimes(1); }); }); describe("sendWebsocketEvent", () => { test("calls correct endpoint with request body", async () => { cli.request.mockResolvedValueOnce({ data: undefined }); const request = { externalUserID: "user123", event: { type: "test" }, transient: true }; await mockClient.sendWebsocketEvent(request); expect(cli.request).toHaveBeenCalledTimes(1); const call = cli.request.mock.calls[0][0]; expect(call.method).toBe("POST"); expect(call.url).toBe("/api/v1/ws/event"); expect(call.data).toEqual(request); }); test("bubbles up request error", async () => { cli.request.mockRejectedValueOnce(new Error("Bad request")); await expect(mockClient.sendWebsocketEvent({ event: { type: "test" } })).rejects.toThrow("Bad request"); }); }); describe("revokeWebsocketConnection", () => { test("calls correct endpoint with id path param", async () => { cli.request.mockResolvedValueOnce({ data: {} }); await mockClient.revokeWebsocketConnection("abc-123"); expect(cli.request).toHaveBeenCalledTimes(1); const call = cli.request.mock.calls[0][0]; expect(call.method).toBe("POST"); expect(call.url).toBe("/api/v1/ws/user/revoke/abc-123"); }); test("bubbles up request error", async () => { cli.request.mockRejectedValueOnce(new Error("Not found")); await expect(mockClient.revokeWebsocketConnection("bad-id")).rejects.toThrow("Not found"); }); }); }); describe("GetBidAskForBIST", () => { it( "should receive BIST bid/ask data", async () => { const symbols = ["AKBNK"]; let receivedData: BISTBidAskStreamData | null = null; let receivedError: Error | null = null; const lc = client.getBidAskForBIST(symbols); activeConnections.push(lc); try { const receiveChan = lc.receive(); const timeoutPromise = new Promise<void>((_, reject) => { const timeout = setTimeout( () => reject(new Error("Timeout waiting for bid/ask data")), TEST_CONSTANTS.MAIN_TIMEOUT ); activeTimeouts.push(timeout); }); const dataPromise = (async () => { try { for await (const data of receiveChan) { receivedData = data; break; } } catch (error) { console.log("Error in bid/ask data stream:", error); } })(); await Promise.race([dataPromise, timeoutPromise]); if (receivedData != null) { const tempReceivedData = (receivedData as BISTBidAskStreamData).d; console.log("Received BIST bid/ask data:", tempReceivedData); expect(tempReceivedData.s).toBeDefined(); expect(typeof tempReceivedData.s).toBe("string"); expect(typeof tempReceivedData.d).toBe("number"); expect(typeof tempReceivedData.ask).toBe("number"); expect(typeof tempReceivedData.bid).toBe("number"); } else { console.log("Timeout waiting for BIST bid/ask data"); } } catch (error) { receivedError = error as Error; console.log("Received bid/ask error:", receivedError.message); } finally { lc.close(); } }, TEST_CONSTANTS.JEST_TIMEOUT ); }); });