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
text/typescript
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
);
});
});