UNPKG

laplace-api

Version:

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

543 lines (451 loc) 19.5 kB
import { Logger } from "winston"; import axios from "axios"; import { LaplaceConfiguration } from "../utilities/configuration"; import { NewsClient, NewsType, NewsOrderBy, } from "../client/news"; import "./client_test_suite"; import { Region, Locale } from "../client/collections"; import { SortDirection } from "../client/broker"; const mockNewsHighlightsResponse = { tech: [ "Alphabet ve Amazon'un desteğiyle Anthropic, 2026 başlarında Hindistan'ın Bengaluru kentinde bir ofis açacak." ], other: [ "ABD Yüksek Mahkemesi, Epic Games'in davası kapsamında Google'ın Play uygulamalarındaki değişikliği engellemeyecek." ], finance: [ "Fifth Third Bank, Comerica'yı 10,9 milyar dolara satın alacak ve böylece ABD'nin 9. en büyük bankası olacak." ], consumer: [ "Tesla, rekabet ortamında pazar payını geri almak için daha ucuz Model Y ve Model 3'ü piyasaya sürdü; duyuru hisseleri etkiledi." ], healthcare: [ "İlaç üreticileri, Amgen ve Novo Nordisk'in de dahil olduğu şekilde, Trump'ın ilaç fiyatlarını düşürme planıyla uyumlu olarak tele-sağlık satışlarını artırıyor." ], energyAndUtilities: [ "ABD Enerji Bakanlığı, Stellantis ve GM'ye verilen 1,1 milyar dolarlık hibeleri iptal edebilir." ], industrialsAndMaterials: [ "Boeing, bir grevi sona erdirmek için IAM Sendikası ile geçici bir anlaşmaya vardı; detaylar açıklanmadı." ] }; const mockNewsResponse = { items: [ { url: "https://www.reuters.com/business/energy/commonwealth-lng-wants-more-time-build-planned-export-facility-louisiana-2025-10-07/", content: { title: "Commonwealth LNG wants more time to build planned export facility in Louisiana", content: [ "Commonwealth LNG has requested a four-year extension from federal regulators to construct & begin exporting liquefied natural gas..." ], summary: [ "Commonwealth LNG has requested a four-year extension from federal regulators..." ], description: "Commonwealth LNG has asked federal regulators for a four-year extension...", investorInsight: "What it means for investors: The extension request could postpone..." }, sectors: { name: "Energy", meanType: 9, newsCount: 1 }, tickers: [{ id: "6203d1ba1e674875275558f7", name: "EQT Corp", symbol: "EQT" }], imageUrl: "", createdAt: "2025-10-07T17:10:01.560644Z", publisher: { name: "Reuters", logoUrl: null }, timestamp: "2025-10-07T16:50:16Z", categories: { name: "Sector News", newsCount: 1, categoryType: "StockSpesific" }, industries: { name: "Oil/Gas (Production and Exploration)", meanType: 78 }, publisherUrl: "Reuters", qualityScore: 0, relatedTickers: [{ id: "6203d1ba1e674875275558f7", name: "EQT Corp", symbol: "EQT" }] } ], recordCount: 352 }; const mockNewsV2Response = { items: mockNewsResponse.items.map(({ relatedTickers, ...rest }) => rest), recordCount: mockNewsResponse.recordCount }; describe("NewsClient", () => { let client: NewsClient; beforeAll(() => { const config = (global as any).testSuite.config as LaplaceConfiguration; const logger: Logger = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), } as unknown as Logger; client = new NewsClient(config, logger); }); describe("Integration Tests", () => { jest.setTimeout(60_000); test("getHighlights returns valid data", async () => { const resp = await client.getHighlights(Region.Us, Locale.Tr); expect(resp).toBeDefined(); expect(Array.isArray(resp.consumer)).toBe(true); expect(Array.isArray(resp.energyAndUtilities)).toBe(true); expect(Array.isArray(resp.finance)).toBe(true); expect(Array.isArray(resp.healthcare)).toBe(true); expect(Array.isArray(resp.industrialsAndMaterials)).toBe(true); expect(Array.isArray(resp.tech)).toBe(true); expect(Array.isArray(resp.other)).toBe(true); const first = resp.tech?.[0]; if (first != null) expect(typeof first).toBe("string"); }); test("getNews returns valid paginated data", async () => { const resp = await client.getNews( Region.Us, Locale.Tr, NewsType.BRIEFS, 0, 10, NewsOrderBy.TIMESTAMP, SortDirection.Desc ); expect(resp).toBeDefined(); expect(typeof resp.recordCount).toBe("number"); expect(resp.recordCount).toBeGreaterThanOrEqual(0); expect(Array.isArray(resp.items)).toBe(true); if (resp.items.length > 0) { const n = resp.items[0]; expect(typeof n.url).toBe("string"); expect(typeof n.imageUrl).toBe("string"); expect(typeof n.timestamp).toBe("string"); expect(typeof n.publisherUrl).toBe("string"); expect(typeof n.qualityScore).toBe("number"); expect(typeof n.createdAt).toBe("string"); expect(n.publisher).toBeDefined(); expect(typeof n.publisher.name).toBe("string"); expect( typeof n.publisher.logoUrl === "string" || n.publisher.logoUrl == null ).toBe(true); expect(Array.isArray(n.relatedTickers)).toBe(true); if (n.relatedTickers.length > 0) { const t = n.relatedTickers[0]; expect(typeof t.id).toBe("string"); expect(typeof t.name).toBe("string"); expect(typeof t.symbol === "string" || t.symbol == null).toBe(true); } if (n.tickers != null) { expect(Array.isArray(n.tickers)).toBe(true); } if (n.categories != null) { expect(typeof n.categories.name).toBe("string"); expect(typeof n.categories.newsCount).toBe("number"); expect( typeof n.categories.categoryType === "string" || n.categories.categoryType == null || n.categories.categoryType === undefined ).toBe(true); expect( typeof n.categories.meanType === "number" || n.categories.meanType == null || n.categories.meanType === undefined ).toBe(true); } if (n.sectors != null) { expect(typeof n.sectors.name).toBe("string"); expect(typeof n.sectors.newsCount).toBe("number"); expect( typeof n.sectors.categoryType === "string" || n.sectors.categoryType == null || n.sectors.categoryType === undefined ).toBe(true); expect( typeof n.sectors.meanType === "number" || n.sectors.meanType == null || n.sectors.meanType === undefined ).toBe(true); } if (n.industries != null) { expect(typeof n.industries.name).toBe("string"); expect(typeof n.industries.meanType).toBe("number"); } if (n.content != null) { expect(typeof n.content.title).toBe("string"); expect(typeof n.content.description).toBe("string"); expect(Array.isArray(n.content.content)).toBe(true); expect(Array.isArray(n.content.summary)).toBe(true); expect(typeof n.content.investorInsight).toBe("string"); } } }); test("getNewsV2 returns valid paginated data", async () => { const resp = await client.getNewsV2( Region.Us, Locale.Tr, NewsType.BRIEFS, 0, 10, NewsOrderBy.TIMESTAMP, SortDirection.Desc ); expect(resp).toBeDefined(); expect(typeof resp.recordCount).toBe("number"); expect(resp.recordCount).toBeGreaterThanOrEqual(0); expect(Array.isArray(resp.items)).toBe(true); if (resp.items.length > 0) { const n = resp.items[0]; expect(typeof n.url).toBe("string"); expect(typeof n.imageUrl).toBe("string"); expect(typeof n.timestamp).toBe("string"); expect(typeof n.publisherUrl).toBe("string"); expect(typeof n.qualityScore).toBe("number"); expect(typeof n.createdAt).toBe("string"); expect((n as any).relatedTickers).toBeUndefined(); expect(n.publisher).toBeDefined(); expect(typeof n.publisher.name).toBe("string"); } }); test("streamNews yields item before timeout or throws gracefully if none arrive", async () => { let newsItemsReceived = 0; const { events, cancel } = client.streamNews(Region.Us, Locale.Tr); const receivePromise = (async () => { for await (const items of events) { if (items && items.length > 0) { newsItemsReceived += items.length; break; } } })(); const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 8000)); await Promise.race([receivePromise, timeoutPromise]); cancel(); expect(newsItemsReceived).toBeGreaterThanOrEqual(0); }); }); describe("Mock Tests", () => { let client: NewsClient; let cli: { request: jest.Mock }; beforeEach(() => { cli = { request: jest.fn() }; const config = (global as any).testSuite.config as LaplaceConfiguration; const logger: Logger = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() } as unknown as Logger; client = new NewsClient(config, logger, cli as any); }); describe("getHighlights", () => { test("calls correct endpoint/params and matches raw response", async () => { cli.request.mockResolvedValueOnce({ data: mockNewsHighlightsResponse }); const resp = await client.getHighlights(Region.Tr, Locale.Tr); expect(cli.request).toHaveBeenCalledTimes(1); const call = cli.request.mock.calls[0][0]; expect(call.method).toBe("GET"); expect(call.url).toBe("/api/v1/news/highlights"); expect(call.params).toEqual({ region: Region.Tr, locale: Locale.Tr }); expect(resp.consumer).toEqual(mockNewsHighlightsResponse.consumer); expect(resp.energyAndUtilities).toEqual(mockNewsHighlightsResponse.energyAndUtilities); expect(resp.finance).toEqual(mockNewsHighlightsResponse.finance); expect(resp.healthcare).toEqual(mockNewsHighlightsResponse.healthcare); expect(resp.industrialsAndMaterials).toEqual(mockNewsHighlightsResponse.industrialsAndMaterials); expect(resp.tech).toEqual(mockNewsHighlightsResponse.tech); expect(resp.other).toEqual(mockNewsHighlightsResponse.other); }); test("bubbles up request error", async () => { cli.request.mockRejectedValueOnce(new Error("Failed to fetch highlights")); await expect(client.getHighlights(Region.Tr, Locale.Tr)).rejects.toThrow( "Failed to fetch highlights" ); expect(cli.request).toHaveBeenCalledTimes(1); }); }); describe("getNews", () => { test("calls correct endpoint/params and matches raw response", async () => { cli.request.mockResolvedValueOnce({ data: mockNewsResponse }); const resp = await client.getNews( Region.Tr, Locale.Tr, NewsType.BRIEFS, 1, 10, NewsOrderBy.TIMESTAMP, SortDirection.Desc, undefined ); expect(cli.request).toHaveBeenCalledTimes(1); const call = cli.request.mock.calls[0][0]; expect(call.method).toBe("GET"); expect(call.url).toBe("/api/v1/news"); expect(call.params).toEqual({ region: Region.Tr, locale: Locale.Tr, newsType: NewsType.BRIEFS, page: 1, size: 10, orderBy: NewsOrderBy.TIMESTAMP, orderByDirection: SortDirection.Desc }); expect(resp.recordCount).toBe(352); expect(resp.items).toHaveLength(1); const n = resp.items[0]; expect(n.url).toBe(mockNewsResponse.items[0].url); expect(n.imageUrl).toBe(mockNewsResponse.items[0].imageUrl); expect(n.timestamp).toBe(mockNewsResponse.items[0].timestamp); expect(n.publisherUrl).toBe(mockNewsResponse.items[0].publisherUrl); expect(n.qualityScore).toBe(mockNewsResponse.items[0].qualityScore); expect(n.createdAt).toBe(mockNewsResponse.items[0].createdAt); expect(n.publisher.name).toBe(mockNewsResponse.items[0].publisher.name); expect(n.publisher.logoUrl).toBeNull(); expect(n.relatedTickers).toHaveLength(1); expect(n.relatedTickers[0].id).toBe(mockNewsResponse.items[0].relatedTickers[0].id); expect(n.relatedTickers[0].name).toBe(mockNewsResponse.items[0].relatedTickers[0].name); expect(n.relatedTickers[0].symbol).toBe(mockNewsResponse.items[0].relatedTickers[0].symbol); expect(n.tickers).toHaveLength(1); expect(n.tickers![0].symbol).toBe("EQT"); expect(n.categories?.name).toBe(mockNewsResponse.items[0].categories.name); expect(n.categories?.newsCount).toBe(mockNewsResponse.items[0].categories.newsCount); expect(n.categories?.categoryType).toBe(mockNewsResponse.items[0].categories.categoryType); expect(n.sectors?.name).toBe(mockNewsResponse.items[0].sectors.name); expect(n.sectors?.newsCount).toBe(mockNewsResponse.items[0].sectors.newsCount); expect(n.sectors?.meanType).toBe(mockNewsResponse.items[0].sectors.meanType); expect(n.industries?.name).toBe(mockNewsResponse.items[0].industries.name); expect(n.industries?.meanType).toBe(mockNewsResponse.items[0].industries.meanType); expect(n.content?.title).toBe(mockNewsResponse.items[0].content.title); expect(n.content?.description).toBe(mockNewsResponse.items[0].content.description); expect(n.content?.content).toEqual(mockNewsResponse.items[0].content.content); expect(n.content?.summary).toEqual(mockNewsResponse.items[0].content.summary); expect(n.content?.investorInsight).toBe(mockNewsResponse.items[0].content.investorInsight); }); test("does not send optional params when undefined", async () => { cli.request.mockResolvedValueOnce({ data: mockNewsResponse }); await client.getNews(Region.Tr, Locale.Tr); const call = cli.request.mock.calls[0][0]; expect(call.params).toEqual({ region: Region.Tr, locale: Locale.Tr }); }); test("bubbles up request error", async () => { cli.request.mockRejectedValueOnce(new Error("Failed to fetch news")); await expect( client.getNews( Region.Tr, Locale.Tr, NewsType.REUTERS, 0, 10, NewsOrderBy.TIMESTAMP, SortDirection.Desc ) ).rejects.toThrow("Failed to fetch news"); expect(cli.request).toHaveBeenCalledTimes(1); }); }); describe("getNewsV2", () => { test("calls correct endpoint/params and matches raw response", async () => { cli.request.mockResolvedValueOnce({ data: mockNewsV2Response }); const resp = await client.getNewsV2( Region.Tr, Locale.Tr, NewsType.BRIEFS, 1, 10, NewsOrderBy.TIMESTAMP, SortDirection.Desc, undefined ); expect(cli.request).toHaveBeenCalledTimes(1); const call = cli.request.mock.calls[0][0]; expect(call.method).toBe("GET"); expect(call.url).toBe("/api/v2/news"); expect(call.params).toEqual({ region: Region.Tr, locale: Locale.Tr, newsType: NewsType.BRIEFS, page: 1, size: 10, orderBy: NewsOrderBy.TIMESTAMP, orderByDirection: SortDirection.Desc }); expect(resp.recordCount).toBe(352); expect(resp.items).toHaveLength(1); const n = resp.items[0]; expect((n as any).relatedTickers).toBeUndefined(); expect(n.url).toBe(mockNewsV2Response.items[0].url); }); test("does not send optional params when undefined", async () => { cli.request.mockResolvedValueOnce({ data: mockNewsV2Response }); await client.getNewsV2(Region.Tr, Locale.Tr); const call = cli.request.mock.calls[0][0]; expect(call.params).toEqual({ region: Region.Tr, locale: Locale.Tr }); }); test("bubbles up request error", async () => { cli.request.mockRejectedValueOnce(new Error("Failed to fetch news v2")); await expect( client.getNewsV2( Region.Tr, Locale.Tr, NewsType.REUTERS, 0, 10, NewsOrderBy.TIMESTAMP, SortDirection.Desc ) ).rejects.toThrow("Failed to fetch news v2"); expect(cli.request).toHaveBeenCalledTimes(1); }); }); describe("streamNews", () => { test("calls correct endpoint/params and correctly yields stream entities", async () => { const eventsList: any[] = []; // Mock get response to return a readable stream const mockStreamData = [ "data: " + JSON.stringify([{ url: "http://example.com/stream-news-1", publiser: { name: "test-publisher" } }]) + "\n\n", "data: " + JSON.stringify([{ url: "http://example.com/stream-news-2", publiser: { name: "test-publisher-2" } }]) + "\n\n", ]; const mockAsyncIterator = { async *[Symbol.asyncIterator]() { for (const chunk of mockStreamData) { yield new TextEncoder().encode(chunk); } } }; const axiosGetSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({ data: mockAsyncIterator }); const { events, cancel } = client.streamNews(Region.Us, Locale.Tr); for await (const newsList of events) { eventsList.push(newsList); } expect(axiosGetSpy).toHaveBeenCalledTimes(1); const callArgs = axiosGetSpy.mock.calls[0]; expect(callArgs[0]).toBe(`${client["baseUrl"]}/api/v1/news/stream?locale=tr&region=us`); expect(callArgs[1]?.responseType).toBe('stream'); expect(eventsList).toHaveLength(2); expect(eventsList[0][0].url).toBe("http://example.com/stream-news-1"); expect(eventsList[1][0].url).toBe("http://example.com/stream-news-2"); cancel(); axiosGetSpy.mockRestore(); }); test("calls correct endpoint with optional parameters", async () => { const mockAsyncIterator = { async *[Symbol.asyncIterator]() { yield new TextEncoder().encode("data: " + JSON.stringify([]) + "\n\n"); } }; const axiosGetSpy = jest.spyOn(axios, 'get').mockResolvedValueOnce({ data: mockAsyncIterator }); const { events, cancel } = client.streamNews(Region.Us, Locale.En, ["tech"], ["AAPL"], ["category"], ["software"]); for await (const _ of events) { break; } expect(axiosGetSpy).toHaveBeenCalledTimes(1); const callArgs = axiosGetSpy.mock.calls[0]; expect(callArgs[0]).toBe(`${client["baseUrl"]}/api/v1/news/stream?locale=en&region=us&sectors=tech&tickers=AAPL&categories=category&industries=software`); cancel(); axiosGetSpy.mockRestore(); }); }); }); });