laplace-api 4.7.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,142 +1,197 @@
1
1
  import { Logger } from "winston";
2
2
  import { LaplaceConfiguration } from "../utilities/configuration";
3
- import { CollectionClient, Locale, Region, Collection, CollectionDetail, CollectionType, CollectionPriceGraph } from "../client/collections";
3
+ import {
4
+ CollectionClient,
5
+ Locale,
6
+ Region
7
+ } from "../client/collections";
4
8
  import "./client_test_suite";
5
9
  import { validateCollection, validateCollectionDetail } from "./helpers";
6
- import { Stock, AssetType, PriceDataPoint, HistoricalPricePeriod } from "../client/stocks";
10
+ import {
11
+ AggregateGraphPeriod,
12
+ SortBy,
13
+ } from "../client/collections";
7
14
 
8
- const mockStocks: Stock[] = [
15
+ const mockCollectionList = [
9
16
  {
10
- id: "stock1",
11
- name: "Tüpraş",
12
- symbol: "TUPRS",
13
- assetType: AssetType.Stock,
14
- sectorId: "sector1",
15
- industryId: "industry1",
16
- updatedDate: "2024-03-14T10:00:00Z",
17
- active: true
17
+ id: "620f455a0187ade00bb0d55f",
18
+ title: "En Büyükler",
19
+ region: ["tr"],
20
+ imageUrl:
21
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/en-buyukler_original.webp",
22
+ avatarUrl:
23
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/en-buyukler_avatar.webp",
24
+ numStocks: 10,
25
+ assetClass: "equity",
26
+ order: 0,
18
27
  },
19
- {
20
- id: "stock2",
21
- name: "Garanti Bankası",
22
- symbol: "GARAN",
23
- assetType: AssetType.Stock,
24
- sectorId: "sector2",
25
- industryId: "industry2",
26
- updatedDate: "2024-03-14T10:00:00Z",
27
- active: true
28
- }
29
28
  ];
30
29
 
31
- const mockIndustries: Collection[] = [
30
+ const mockCollectionDetail = {
31
+ id: "620f455a0187ade00bb0d55f",
32
+ title: "En Büyükler",
33
+ region: ["tr"],
34
+ imageUrl:
35
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/en-buyukler_original.webp",
36
+ avatarUrl:
37
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/en-buyukler_avatar.webp",
38
+ numStocks: 10,
39
+ assetClass: "equity",
40
+ order: 0,
41
+ stocks: [
42
+ {
43
+ id: "61dd0d670ec2114146342fa5",
44
+ assetType: "stock",
45
+ name: "SASA Polyester",
46
+ symbol: "SASA",
47
+ sectorId: "65533e047844ee7afe9941c0",
48
+ industryId: "65533e441fa5c7b58afa097a",
49
+ updatedDate: "2025-08-05T14:53:59.57Z",
50
+ active: true,
51
+ },
52
+ ],
53
+ };
54
+
55
+ const mockThemesList = [
32
56
  {
33
- id: "industry1",
34
- title: "Bankacılık",
35
- description: "Türkiye'nin önde gelen bankaları",
36
- region: [Region.Tr],
57
+ id: "6256b0647d0bb100123effa7",
58
+ title: "İhracat Şampiyonları",
59
+ region: ["tr"],
60
+ imageUrl:
61
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/ihracat-sampiyonlari_original.webp",
62
+ avatarUrl:
63
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/ihracat-sampiyonlari_avatar.webp",
64
+ numStocks: 15,
37
65
  assetClass: "equity",
38
- imageUrl: "https://example.com/banking.jpg",
39
- avatarUrl: "https://example.com/banking-avatar.jpg",
40
- numStocks: 10
41
66
  },
42
- {
43
- id: "industry2",
44
- title: "Enerji",
45
- description: "Enerji sektörü şirketleri",
46
- region: [Region.Tr],
47
- assetClass: "equity",
48
- imageUrl: "https://example.com/energy.jpg",
49
- avatarUrl: "https://example.com/energy-avatar.jpg",
50
- numStocks: 8
51
- }
52
67
  ];
53
68
 
54
- const mockSectors: Collection[] = [
55
- {
56
- id: "sector1",
57
- title: "Finans",
58
- description: "Finans sektörü şirketleri",
59
- region: [Region.Tr],
60
- assetClass: "equity",
61
- imageUrl: "https://example.com/finance.jpg",
62
- avatarUrl: "https://example.com/finance-avatar.jpg",
63
- numStocks: 25
64
- }
65
- ];
69
+ const mockThemeDetail = {
70
+ id: "6256b0647d0bb100123effa7",
71
+ title: "İhracat Şampiyonları",
72
+ region: ["tr"],
73
+ imageUrl:
74
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/ihracat-sampiyonlari_original.webp",
75
+ avatarUrl:
76
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/ihracat-sampiyonlari_avatar.webp",
77
+ numStocks: 15,
78
+ assetClass: "equity",
79
+ image:
80
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/themes/tr/ihracat_sampiyonlari.png",
81
+ order: 0,
82
+ stocks: [
83
+ {
84
+ id: "61dd0d4b0ec2114146342f69",
85
+ assetType: "stock",
86
+ name: "Şişe ve Cam Fabrikaları",
87
+ symbol: "SISE",
88
+ sectorId: "65533e047844ee7afe9941be",
89
+ industryId: "65533e441fa5c7b58afa0956",
90
+ updatedDate: "2025-04-01T00:00:00.533Z",
91
+ active: true,
92
+ },
93
+ ],
94
+ };
66
95
 
67
- const mockThemes: Collection[] = [
96
+ const mockIndustryList = [
68
97
  {
69
- id: "theme1",
70
- title: "Sürdürülebilir Enerji",
71
- description: "Yenilenebilir enerji şirketleri",
72
- region: [Region.Tr],
73
- assetClass: "equity",
74
- imageUrl: "https://example.com/sustainable.jpg",
75
- avatarUrl: "https://example.com/sustainable-avatar.jpg",
76
- numStocks: 15
77
- }
98
+ id: "65533e441fa5c7b58afa097b",
99
+ title: "Kimyasal (Çeşitlendirilmiş)",
100
+ imageUrl:
101
+ "https://finfree-storage.s3.amazonaws.com/collection-images/chemical-diversified.webp",
102
+ avatarUrl:
103
+ "https://finfree-storage.s3.amazonaws.com/collection-images/chemical-diversified_avatar.webp",
104
+ numStocks: 1,
105
+ },
78
106
  ];
79
107
 
80
- const mockCollections: Collection[] = [
108
+ const mockIndustryDetail = {
109
+ id: "65533e441fa5c7b58afa0944",
110
+ title: "Perakende (Özel Hatlar)",
111
+ region: ["us", "tr"],
112
+ imageUrl:
113
+ "https://finfree-storage.s3.amazonaws.com/collection-images/retail-special-lines.webp",
114
+ avatarUrl:
115
+ "https://finfree-storage.s3.amazonaws.com/collection-images/retail-special-lines_avatar.webp",
116
+ numStocks: 59,
117
+ order: 0,
118
+ stocks: [
119
+ {
120
+ id: "61dd0d850ec2114146343053",
121
+ assetType: "stock",
122
+ name: "Teknosa",
123
+ symbol: "TKNSA",
124
+ sectorId: "65533e047844ee7afe9941b9",
125
+ industryId: "65533e441fa5c7b58afa0944",
126
+ updatedDate: "2025-07-02T00:00:00.426Z",
127
+ active: true,
128
+ },
129
+ ],
130
+ };
131
+
132
+ const mockSectorsList = [
81
133
  {
82
- id: "collection1",
83
- title: "Borsa İstanbul 30",
84
- description: "BIST-30 endeksindeki şirketler",
85
- region: [Region.Tr],
86
- assetClass: "equity",
87
- imageUrl: "https://example.com/bist30.jpg",
88
- avatarUrl: "https://example.com/bist30-avatar.jpg",
89
- numStocks: 30
90
- }
134
+ id: "65533e047844ee7afe9941bf",
135
+ title: "Teknoloji",
136
+ imageUrl:
137
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/informationtechnology-stocks_original.webp",
138
+ avatarUrl:
139
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/informationtechnology-stocks_avatar.webp",
140
+ numStocks: 47,
141
+ },
91
142
  ];
92
143
 
93
- const mockPriceDataPoints: PriceDataPoint[] = [
144
+ const mockSectorDetail = {
145
+ id: "65533e047844ee7afe9941b9",
146
+ title: "Tüketici Döngüsel",
147
+ region: ["us", "tr"],
148
+ imageUrl:
149
+ "https://finfree-storage.s3.amazonaws.com/collection-images/consumer-cyclical.webp",
150
+ avatarUrl:
151
+ "https://finfree-storage.s3.amazonaws.com/collection-images/consumer-cyclical_avatar.webp",
152
+ numStocks: 798,
153
+ order: 0,
154
+ stocks: [
155
+ {
156
+ id: "61dd0d420ec2114146342f2d",
157
+ assetType: "stock",
158
+ name: "Tek-Art İnşaat",
159
+ symbol: "TEKTU",
160
+ sectorId: "65533e047844ee7afe9941b9",
161
+ industryId: "65533e441fa5c7b58afa093b",
162
+ updatedDate: "2022-01-11T04:53:22.944Z",
163
+ active: true,
164
+ },
165
+ ],
166
+ };
167
+
168
+ const mockPriceDataPoints = [
94
169
  {
95
170
  d: 1710403200000,
96
171
  o: 100.5,
97
172
  h: 105.2,
98
173
  l: 99.8,
99
- c: 103.7
174
+ c: 103.7,
100
175
  },
101
176
  {
102
177
  d: 1710489600000,
103
178
  o: 103.7,
104
179
  h: 107.1,
105
180
  l: 102.3,
106
- c: 106.4
181
+ c: 106.4,
107
182
  },
108
183
  {
109
184
  d: 1710576000000,
110
185
  o: 106.4,
111
186
  h: 108.9,
112
187
  l: 105.1,
113
- c: 107.8
114
- }
188
+ c: 107.8,
189
+ },
115
190
  ];
116
191
 
117
- const mockCollectionPriceGraph: CollectionPriceGraph = {
192
+ const mockCollectionPriceGraph = {
118
193
  previous_close: 98.5,
119
- graph: mockPriceDataPoints
120
- };
121
-
122
- const mockIndustryDetail: CollectionDetail = {
123
- ...mockIndustries[0],
124
- stocks: mockStocks
125
- };
126
-
127
- const mockSectorDetail: CollectionDetail = {
128
- ...mockSectors[0],
129
- stocks: mockStocks
130
- };
131
-
132
- const mockThemeDetail: CollectionDetail = {
133
- ...mockThemes[0],
134
- stocks: mockStocks
135
- };
136
-
137
- const mockCollectionDetail: CollectionDetail = {
138
- ...mockCollections[0],
139
- stocks: mockStocks
194
+ graph: mockPriceDataPoints,
140
195
  };
141
196
 
142
197
  describe("Collections", () => {
@@ -218,7 +273,7 @@ describe("Collections", () => {
218
273
  });
219
274
 
220
275
  test("GetCollectionDetails", async () => {
221
- const resp = await client.getThemeDetail(
276
+ const resp = await client.getCollectionDetail(
222
277
  "620f455a0187ade00bb0d55f",
223
278
  Region.Tr,
224
279
  Locale.Tr
@@ -229,18 +284,18 @@ describe("Collections", () => {
229
284
 
230
285
  test("GetAggregateGraph", async () => {
231
286
  const resp = await client.getAggregateGraph(
232
- HistoricalPricePeriod.OneYear,
287
+ AggregateGraphPeriod.OneYear,
233
288
  "65533e047844ee7afe9941b9",
234
289
  "65533e441fa5c7b58afa0944",
235
290
  "",
236
291
  Region.Tr
237
292
  );
238
-
293
+
239
294
  expect(resp).not.toBeNull();
240
295
  expect(resp.previous_close).toBeDefined();
241
296
  expect(resp.graph).toBeDefined();
242
297
  expect(resp.graph).toBeInstanceOf(Array);
243
-
298
+
244
299
  if (resp.graph.length > 0) {
245
300
  const firstDataPoint = resp.graph[0];
246
301
  expect(firstDataPoint.d).toBeDefined();
@@ -248,231 +303,397 @@ describe("Collections", () => {
248
303
  expect(firstDataPoint.h).toBeDefined();
249
304
  expect(firstDataPoint.l).toBeDefined();
250
305
  expect(firstDataPoint.c).toBeDefined();
251
- expect(typeof firstDataPoint.d).toBe('number');
252
- expect(typeof firstDataPoint.o).toBe('number');
253
- expect(typeof firstDataPoint.h).toBe('number');
254
- expect(typeof firstDataPoint.l).toBe('number');
255
- expect(typeof firstDataPoint.c).toBe('number');
306
+ expect(typeof firstDataPoint.d).toBe("number");
307
+ expect(typeof firstDataPoint.o).toBe("number");
308
+ expect(typeof firstDataPoint.h).toBe("number");
309
+ expect(typeof firstDataPoint.l).toBe("number");
310
+ expect(typeof firstDataPoint.c).toBe("number");
256
311
  }
257
312
  });
258
313
  });
259
314
 
260
315
  describe("Mock Tests", () => {
261
- beforeEach(() => {
262
- jest.clearAllMocks();
263
- });
316
+ let client: CollectionClient;
317
+ let cli: { request: jest.Mock };
264
318
 
265
- describe("Industries", () => {
266
- test("should get all industries", async () => {
267
- jest.spyOn(client, 'getAllIndustries').mockResolvedValue(mockIndustries);
319
+ const region = Region.Tr;
320
+ const locale = Locale.Tr;
268
321
 
269
- const resp = await client.getAllIndustries(Region.Tr, Locale.Tr);
322
+ beforeEach(() => {
323
+ cli = { request: jest.fn() };
270
324
 
271
- expect(resp).toHaveLength(2);
272
- expect(resp[0].title).toBe("Bankacılık");
273
- expect(resp[1].title).toBe("Enerji");
274
- expect(resp[0].numStocks).toBe(10);
275
- expect(resp[1].numStocks).toBe(8);
325
+ const config = (global as any).testSuite.config as LaplaceConfiguration;
326
+ const logger: Logger = {
327
+ info: jest.fn(),
328
+ error: jest.fn(),
329
+ warn: jest.fn(),
330
+ debug: jest.fn(),
331
+ } as unknown as Logger;
276
332
 
277
- expect(client.getAllIndustries).toHaveBeenCalledWith(Region.Tr, Locale.Tr);
278
- });
333
+ client = new CollectionClient(config, logger, cli as any);
334
+ });
279
335
 
280
- test("should get industry details", async () => {
281
- jest.spyOn(client, 'getIndustryDetail').mockResolvedValue(mockIndustryDetail);
336
+ describe("getAllIndustries", () => {
337
+ test("calls correct endpoint/params and matches raw list", async () => {
338
+ cli.request.mockResolvedValueOnce({ data: mockIndustryList });
282
339
 
283
- const resp = await client.getIndustryDetail("industry1", Region.Tr, Locale.Tr);
340
+ const resp = await client.getAllIndustries(region, locale);
284
341
 
285
- expect(resp.id).toBe("industry1");
286
- expect(resp.title).toBe("Bankacılık");
287
- expect(resp.stocks).toHaveLength(2);
288
- expect(resp.stocks[0].symbol).toBe("TUPRS");
289
- expect(resp.stocks[1].symbol).toBe("GARAN");
342
+ expect(cli.request).toHaveBeenCalledTimes(1);
343
+ const call = cli.request.mock.calls[0][0];
344
+ expect(call.method).toBe("GET");
345
+ expect(call.url).toBe("/api/v1/industry");
346
+ expect(call.params).toEqual({ region, locale });
290
347
 
291
- expect(client.getIndustryDetail).toHaveBeenCalledWith("industry1", Region.Tr, Locale.Tr);
348
+ expect(resp).toHaveLength(1);
349
+ const item = resp[0];
350
+ expect(item.id).toBe("65533e441fa5c7b58afa097b");
351
+ expect(item.title).toBe("Kimyasal (Çeşitlendirilmiş)");
352
+ expect(item.imageUrl).toBe(
353
+ "https://finfree-storage.s3.amazonaws.com/collection-images/chemical-diversified.webp"
354
+ );
355
+ expect(item.avatarUrl).toBe(
356
+ "https://finfree-storage.s3.amazonaws.com/collection-images/chemical-diversified_avatar.webp"
357
+ );
358
+ expect(item.numStocks).toBe(1);
359
+
360
+ expect((item as any).region).toBeUndefined();
361
+ expect((item as any).assetClass).toBeUndefined();
362
+ expect((item as any).order).toBeUndefined();
292
363
  });
293
364
  });
294
365
 
295
- describe("Sectors", () => {
296
- test("should get all sectors", async () => {
297
- jest.spyOn(client, 'getAllSectors').mockResolvedValue(mockSectors);
298
-
299
- const resp = await client.getAllSectors(Region.Tr, Locale.Tr);
300
-
301
- expect(resp).toHaveLength(1);
302
- expect(resp[0].title).toBe("Finans");
303
- expect(resp[0].numStocks).toBe(25);
304
-
305
- expect(client.getAllSectors).toHaveBeenCalledWith(Region.Tr, Locale.Tr);
366
+ describe("getIndustryDetail", () => {
367
+ test("calls correct endpoint/params and matches raw detail", async () => {
368
+ cli.request.mockResolvedValueOnce({ data: mockIndustryDetail });
369
+
370
+ const resp = await client.getIndustryDetail(
371
+ "65533e441fa5c7b58afa0944",
372
+ region,
373
+ locale
374
+ );
375
+
376
+ expect(cli.request).toHaveBeenCalledTimes(1);
377
+ const call = cli.request.mock.calls[0][0];
378
+ expect(call.method).toBe("GET");
379
+ expect(call.url).toBe("/api/v1/industry/65533e441fa5c7b58afa0944");
380
+ expect(call.params).toEqual({ region, locale });
381
+
382
+ expect(resp.id).toBe("65533e441fa5c7b58afa0944");
383
+ expect(resp.title).toBe("Perakende (Özel Hatlar)");
384
+ expect(resp.region).toEqual(["us", "tr"]);
385
+ expect(resp.imageUrl).toBe(
386
+ "https://finfree-storage.s3.amazonaws.com/collection-images/retail-special-lines.webp"
387
+ );
388
+ expect(resp.avatarUrl).toBe(
389
+ "https://finfree-storage.s3.amazonaws.com/collection-images/retail-special-lines_avatar.webp"
390
+ );
391
+ expect(resp.numStocks).toBe(59);
392
+ expect(resp.order).toBe(0);
393
+
394
+ // stocks
395
+ expect(resp.stocks).toHaveLength(1);
396
+ const s = resp.stocks[0];
397
+ expect(s.id).toBe("61dd0d850ec2114146343053");
398
+ expect((s as any).assetType).toBe("stock");
399
+ expect(s.name).toBe("Teknosa");
400
+ expect(s.symbol).toBe("TKNSA");
401
+ expect(s.sectorId).toBe("65533e047844ee7afe9941b9");
402
+ expect(s.industryId).toBe("65533e441fa5c7b58afa0944");
403
+ expect(s.updatedDate).toBe("2025-07-02T00:00:00.426Z");
404
+ expect(s.active).toBe(true);
306
405
  });
406
+ });
307
407
 
308
- test("should get sector details", async () => {
309
- jest.spyOn(client, 'getSectorDetail').mockResolvedValue(mockSectorDetail);
310
-
311
- const resp = await client.getSectorDetail("sector1", Region.Tr, Locale.Tr);
408
+ describe("detail methods with sortBy", () => {
409
+ test("includes sortBy param when provided", async () => {
410
+ cli.request.mockResolvedValueOnce({ data: mockIndustryDetail });
312
411
 
313
- expect(resp.id).toBe("sector1");
314
- expect(resp.title).toBe("Finans");
315
- expect(resp.stocks).toHaveLength(2);
316
- expect(resp.stocks[0].symbol).toBe("TUPRS");
317
- expect(resp.stocks[1].symbol).toBe("GARAN");
412
+ await client.getIndustryDetail(
413
+ "65533e441fa5c7b58afa0944",
414
+ region,
415
+ locale,
416
+ SortBy.PriceChange
417
+ );
318
418
 
319
- expect(client.getSectorDetail).toHaveBeenCalledWith("sector1", Region.Tr, Locale.Tr);
419
+ const call = cli.request.mock.calls[0][0];
420
+ expect(call.params).toEqual({ region, locale, sortBy: SortBy.PriceChange });
320
421
  });
321
422
  });
322
423
 
323
- describe("Themes", () => {
324
- test("should get all themes", async () => {
325
- jest.spyOn(client, 'getAllThemes').mockResolvedValue(mockThemes);
424
+ describe("getAllSectors", () => {
425
+ test("calls correct endpoint/params and matches raw list", async () => {
426
+ cli.request.mockResolvedValueOnce({ data: mockSectorsList });
326
427
 
327
- const resp = await client.getAllThemes(Region.Tr, Locale.Tr);
428
+ const resp = await client.getAllSectors(region, locale);
328
429
 
329
- expect(resp).toHaveLength(1);
330
- expect(resp[0].title).toBe("Sürdürülebilir Enerji");
331
- expect(resp[0].numStocks).toBe(15);
430
+ expect(cli.request).toHaveBeenCalledTimes(1);
431
+ const call = cli.request.mock.calls[0][0];
432
+ expect(call.method).toBe("GET");
433
+ expect(call.url).toBe("/api/v1/sector");
434
+ expect(call.params).toEqual({ region, locale });
332
435
 
333
- expect(client.getAllThemes).toHaveBeenCalledWith(Region.Tr, Locale.Tr);
436
+ expect(resp).toHaveLength(1);
437
+ const item = resp[0];
438
+ expect(item.id).toBe("65533e047844ee7afe9941bf");
439
+ expect(item.title).toBe("Teknoloji");
440
+ expect(item.imageUrl).toBe(
441
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/informationtechnology-stocks_original.webp"
442
+ );
443
+ expect(item.avatarUrl).toBe(
444
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/informationtechnology-stocks_avatar.webp"
445
+ );
446
+ expect(item.numStocks).toBe(47);
447
+
448
+ expect((item as any).region).toBeUndefined();
449
+ expect((item as any).assetClass).toBeUndefined();
450
+ expect((item as any).order).toBeUndefined();
334
451
  });
452
+ });
335
453
 
336
- test("should get theme details", async () => {
337
- jest.spyOn(client, 'getThemeDetail').mockResolvedValue(mockThemeDetail);
338
-
339
- const resp = await client.getThemeDetail("theme1", Region.Tr, Locale.Tr);
340
-
341
- expect(resp.id).toBe("theme1");
342
- expect(resp.title).toBe("Sürdürülebilir Enerji");
343
- expect(resp.stocks).toHaveLength(2);
344
- expect(resp.stocks[0].symbol).toBe("TUPRS");
345
- expect(resp.stocks[1].symbol).toBe("GARAN");
346
-
347
- expect(client.getThemeDetail).toHaveBeenCalledWith("theme1", Region.Tr, Locale.Tr);
454
+ describe("getSectorDetail", () => {
455
+ test("calls correct endpoint/params and matches raw detail", async () => {
456
+ cli.request.mockResolvedValueOnce({ data: mockSectorDetail });
457
+
458
+ const resp = await client.getSectorDetail(
459
+ "65533e047844ee7afe9941b9",
460
+ region,
461
+ locale
462
+ );
463
+
464
+ expect(cli.request).toHaveBeenCalledTimes(1);
465
+ const call = cli.request.mock.calls[0][0];
466
+ expect(call.method).toBe("GET");
467
+ expect(call.url).toBe("/api/v1/sector/65533e047844ee7afe9941b9");
468
+ expect(call.params).toEqual({ region, locale });
469
+
470
+ expect(resp.id).toBe("65533e047844ee7afe9941b9");
471
+ expect(resp.title).toBe("Tüketici Döngüsel");
472
+ expect(resp.region).toEqual(["us", "tr"]);
473
+ expect(resp.imageUrl).toBe(
474
+ "https://finfree-storage.s3.amazonaws.com/collection-images/consumer-cyclical.webp"
475
+ );
476
+ expect(resp.avatarUrl).toBe(
477
+ "https://finfree-storage.s3.amazonaws.com/collection-images/consumer-cyclical_avatar.webp"
478
+ );
479
+ expect(resp.numStocks).toBe(798);
480
+ expect(resp.order).toBe(0);
481
+
482
+ expect(resp.stocks).toHaveLength(1);
483
+ const s = resp.stocks[0];
484
+ expect(s.id).toBe("61dd0d420ec2114146342f2d");
485
+ expect((s as any).assetType).toBe("stock");
486
+ expect(s.name).toBe("Tek-Art İnşaat");
487
+ expect(s.symbol).toBe("TEKTU");
488
+ expect(s.sectorId).toBe("65533e047844ee7afe9941b9");
489
+ expect(s.industryId).toBe("65533e441fa5c7b58afa093b");
490
+ expect(s.updatedDate).toBe("2022-01-11T04:53:22.944Z");
491
+ expect(s.active).toBe(true);
348
492
  });
349
493
  });
350
494
 
351
- describe("Collections", () => {
352
- test("should get all collections", async () => {
353
- jest.spyOn(client, 'getAllCollections').mockResolvedValue(mockCollections);
495
+ describe("getAllThemes", () => {
496
+ test("calls correct endpoint/params and matches raw list", async () => {
497
+ cli.request.mockResolvedValueOnce({ data: mockThemesList });
354
498
 
355
- const resp = await client.getAllCollections(Region.Tr, Locale.Tr);
499
+ const resp = await client.getAllThemes(region, locale);
500
+
501
+ expect(cli.request).toHaveBeenCalledTimes(1);
502
+ const call = cli.request.mock.calls[0][0];
503
+ expect(call.method).toBe("GET");
504
+ expect(call.url).toBe("/api/v1/theme");
505
+ expect(call.params).toEqual({ region, locale });
356
506
 
357
507
  expect(resp).toHaveLength(1);
358
- expect(resp[0].title).toBe("Borsa İstanbul 30");
359
- expect(resp[0].numStocks).toBe(30);
508
+ const item = resp[0];
509
+ expect(item.id).toBe("6256b0647d0bb100123effa7");
510
+ expect(item.title).toBe("İhracat Şampiyonları");
511
+ expect(item.region).toEqual(["tr"]);
512
+ expect(item.imageUrl).toBe(
513
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/ihracat-sampiyonlari_original.webp"
514
+ );
515
+ expect(item.avatarUrl).toBe(
516
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/ihracat-sampiyonlari_avatar.webp"
517
+ );
518
+ expect(item.numStocks).toBe(15);
519
+ expect(item.assetClass).toBe("equity");
520
+ });
521
+ });
360
522
 
361
- expect(client.getAllCollections).toHaveBeenCalledWith(Region.Tr, Locale.Tr);
523
+ describe("getThemeDetail", () => {
524
+ test("calls correct endpoint/params and matches raw detail", async () => {
525
+ cli.request.mockResolvedValueOnce({ data: mockThemeDetail });
526
+
527
+ const resp = await client.getThemeDetail(
528
+ "6256b0647d0bb100123effa7",
529
+ region,
530
+ locale
531
+ );
532
+
533
+ expect(cli.request).toHaveBeenCalledTimes(1);
534
+ const call = cli.request.mock.calls[0][0];
535
+ expect(call.method).toBe("GET");
536
+ expect(call.url).toBe("/api/v1/theme/6256b0647d0bb100123effa7");
537
+ expect(call.params).toEqual({ region, locale });
538
+
539
+ expect(resp.id).toBe("6256b0647d0bb100123effa7");
540
+ expect(resp.title).toBe("İhracat Şampiyonları");
541
+ expect(resp.region).toEqual(["tr"]);
542
+ expect(resp.imageUrl).toBe(
543
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/ihracat-sampiyonlari_original.webp"
544
+ );
545
+ expect(resp.avatarUrl).toBe(
546
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/ihracat-sampiyonlari_avatar.webp"
547
+ );
548
+ expect(resp.numStocks).toBe(15);
549
+ expect(resp.assetClass).toBe("equity");
550
+ expect(resp.image).toBe(
551
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/themes/tr/ihracat_sampiyonlari.png"
552
+ );
553
+ expect(resp.order).toBe(0);
554
+
555
+ expect(resp.stocks).toHaveLength(1);
556
+ const s = resp.stocks[0];
557
+ expect(s.id).toBe("61dd0d4b0ec2114146342f69");
558
+ expect((s as any).assetType).toBe("stock");
559
+ expect(s.name).toBe("Şişe ve Cam Fabrikaları");
560
+ expect(s.symbol).toBe("SISE");
561
+ expect(s.sectorId).toBe("65533e047844ee7afe9941be");
562
+ expect(s.industryId).toBe("65533e441fa5c7b58afa0956");
563
+ expect(s.updatedDate).toBe("2025-04-01T00:00:00.533Z");
564
+ expect(s.active).toBe(true);
362
565
  });
566
+ });
363
567
 
364
- test("should get collection details", async () => {
365
- jest.spyOn(client, 'getCollectionDetail').mockResolvedValue(mockCollectionDetail);
568
+ describe("getAllCollections", () => {
569
+ test("calls correct endpoint/params and matches raw list", async () => {
570
+ cli.request.mockResolvedValueOnce({ data: mockCollectionList });
366
571
 
367
- const resp = await client.getCollectionDetail("collection1", Region.Tr, Locale.Tr);
572
+ const resp = await client.getAllCollections(region, locale);
368
573
 
369
- expect(resp.id).toBe("collection1");
370
- expect(resp.title).toBe("Borsa İstanbul 30");
371
- expect(resp.stocks).toHaveLength(2);
372
- expect(resp.stocks[0].symbol).toBe("TUPRS");
373
- expect(resp.stocks[1].symbol).toBe("GARAN");
574
+ expect(cli.request).toHaveBeenCalledTimes(1);
575
+ const call = cli.request.mock.calls[0][0];
576
+ expect(call.method).toBe("GET");
577
+ expect(call.url).toBe("/api/v1/collection");
578
+ expect(call.params).toEqual({ region, locale });
374
579
 
375
- expect(client.getCollectionDetail).toHaveBeenCalledWith("collection1", Region.Tr, Locale.Tr);
580
+ expect(resp).toHaveLength(1);
581
+ const item = resp[0];
582
+ expect(item.id).toBe("620f455a0187ade00bb0d55f");
583
+ expect(item.title).toBe("En Büyükler");
584
+ expect(item.region).toEqual(["tr"]);
585
+ expect(item.imageUrl).toBe(
586
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/en-buyukler_original.webp"
587
+ );
588
+ expect(item.avatarUrl).toBe(
589
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/en-buyukler_avatar.webp"
590
+ );
591
+ expect(item.numStocks).toBe(10);
592
+ expect(item.assetClass).toBe("equity");
593
+ expect(item.order).toBe(0);
376
594
  });
377
595
  });
378
596
 
379
- describe("Aggregate Graph", () => {
380
- test("should get aggregate graph for sector", async () => {
381
- jest.spyOn(client, 'getAggregateGraph').mockResolvedValue(mockCollectionPriceGraph);
382
-
383
- const resp = await client.getAggregateGraph(HistoricalPricePeriod.OneMonth, "sector1", "", "", Region.Tr);
597
+ describe("getCollectionDetail", () => {
598
+ test("calls correct endpoint/params and matches raw detail", async () => {
599
+ cli.request.mockResolvedValueOnce({ data: mockCollectionDetail });
600
+
601
+ const resp = await client.getCollectionDetail(
602
+ "620f455a0187ade00bb0d55f",
603
+ region,
604
+ locale
605
+ );
606
+
607
+ expect(cli.request).toHaveBeenCalledTimes(1);
608
+ const call = cli.request.mock.calls[0][0];
609
+ expect(call.method).toBe("GET");
610
+ expect(call.url).toBe("/api/v1/collection/620f455a0187ade00bb0d55f");
611
+ expect(call.params).toEqual({ region, locale });
612
+
613
+ expect(resp.id).toBe("620f455a0187ade00bb0d55f");
614
+ expect(resp.title).toBe("En Büyükler");
615
+ expect(resp.region).toEqual(["tr"]);
616
+ expect(resp.imageUrl).toBe(
617
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/en-buyukler_original.webp"
618
+ );
619
+ expect(resp.avatarUrl).toBe(
620
+ "https://finfree-storage.s3.eu-central-1.amazonaws.com/collection-images/en-buyukler_avatar.webp"
621
+ );
622
+ expect(resp.numStocks).toBe(10);
623
+ expect(resp.assetClass).toBe("equity");
624
+ expect(resp.order).toBe(0);
625
+
626
+ expect(resp.stocks).toHaveLength(1);
627
+ const s = resp.stocks[0];
628
+ expect(s.id).toBe("61dd0d670ec2114146342fa5");
629
+ expect((s as any).assetType).toBe("stock");
630
+ expect(s.name).toBe("SASA Polyester");
631
+ expect(s.symbol).toBe("SASA");
632
+ expect(s.sectorId).toBe("65533e047844ee7afe9941c0");
633
+ expect(s.industryId).toBe("65533e441fa5c7b58afa097a");
634
+ expect(s.updatedDate).toBe("2025-08-05T14:53:59.57Z");
635
+ expect(s.active).toBe(true);
636
+ });
637
+ });
638
+ describe("getAggregateGraph", () => {
639
+ test("calls correct endpoint/params and matches raw graph response", async () => {
640
+ cli.request.mockResolvedValueOnce({ data: mockCollectionPriceGraph });
641
+
642
+ const resp = await client.getAggregateGraph(
643
+ AggregateGraphPeriod.OneYear,
644
+ "65533e047844ee7afe9941b9", // sectorId
645
+ "65533e441fa5c7b58afa0944", // industryId
646
+ "", // collectionId
647
+ Region.Tr
648
+ );
649
+
650
+ expect(cli.request).toHaveBeenCalledTimes(1);
651
+ const call = cli.request.mock.calls[0][0];
652
+
653
+ expect(call.method).toBe("GET");
654
+ expect(call.url).toBe("/api/v1/aggregate/graph");
655
+ expect(call.params).toEqual({
656
+ period: AggregateGraphPeriod.OneYear,
657
+ sectorId: "65533e047844ee7afe9941b9",
658
+ industryId: "65533e441fa5c7b58afa0944",
659
+ collectionId: "",
660
+ region: Region.Tr,
661
+ });
384
662
 
385
663
  expect(resp.previous_close).toBe(98.5);
386
664
  expect(resp.graph).toHaveLength(3);
665
+
666
+ expect(resp.graph[0].d).toBe(1710403200000);
387
667
  expect(resp.graph[0].o).toBe(100.5);
388
668
  expect(resp.graph[0].h).toBe(105.2);
389
669
  expect(resp.graph[0].l).toBe(99.8);
390
670
  expect(resp.graph[0].c).toBe(103.7);
391
- expect(resp.graph[2].c).toBe(107.8);
392
-
393
- expect(client.getAggregateGraph).toHaveBeenCalledWith(HistoricalPricePeriod.OneMonth, "sector1", "", "", Region.Tr);
394
- });
395
-
396
- test("should get aggregate graph for industry", async () => {
397
- jest.spyOn(client, 'getAggregateGraph').mockResolvedValue(mockCollectionPriceGraph);
398
-
399
- const resp = await client.getAggregateGraph(HistoricalPricePeriod.ThreeMonth, "", "industry1", "", Region.Tr);
400
671
 
401
- expect(resp.previous_close).toBe(98.5);
402
- expect(resp.graph).toHaveLength(3);
672
+ expect(resp.graph[1].d).toBe(1710489600000);
403
673
  expect(resp.graph[1].o).toBe(103.7);
404
674
  expect(resp.graph[1].h).toBe(107.1);
405
675
  expect(resp.graph[1].l).toBe(102.3);
406
676
  expect(resp.graph[1].c).toBe(106.4);
407
677
 
408
- expect(client.getAggregateGraph).toHaveBeenCalledWith(HistoricalPricePeriod.ThreeMonth, "", "industry1", "", Region.Tr);
409
- });
410
-
411
- test("should get aggregate graph for collection", async () => {
412
- jest.spyOn(client, 'getAggregateGraph').mockResolvedValue(mockCollectionPriceGraph);
413
-
414
- const resp = await client.getAggregateGraph(HistoricalPricePeriod.OneWeek, "", "", "collection1", Region.Tr);
415
-
416
- expect(resp.previous_close).toBe(98.5);
417
- expect(resp.graph).toHaveLength(3);
418
- expect(resp.graph[0].d).toBe(1710403200000);
419
- expect(resp.graph[1].d).toBe(1710489600000);
420
678
  expect(resp.graph[2].d).toBe(1710576000000);
421
-
422
- expect(client.getAggregateGraph).toHaveBeenCalledWith(HistoricalPricePeriod.OneWeek, "", "", "collection1", Region.Tr);
423
- });
424
-
425
- test("should get aggregate graph for different periods", async () => {
426
- jest.spyOn(client, 'getAggregateGraph').mockResolvedValue(mockCollectionPriceGraph);
427
-
428
- const resp1D = await client.getAggregateGraph(HistoricalPricePeriod.OneDay, "sector1", "", "", Region.Tr);
429
- expect(resp1D.previous_close).toBe(98.5);
430
- expect(client.getAggregateGraph).toHaveBeenCalledWith(HistoricalPricePeriod.OneDay, "sector1", "", "", Region.Tr);
431
-
432
- const resp1Y = await client.getAggregateGraph(HistoricalPricePeriod.OneYear, "", "industry1", "", Region.Tr);
433
- expect(resp1Y.previous_close).toBe(98.5);
434
- expect(client.getAggregateGraph).toHaveBeenCalledWith(HistoricalPricePeriod.OneYear, "", "industry1", "", Region.Tr);
435
-
436
- const resp5Y = await client.getAggregateGraph(HistoricalPricePeriod.FiveYear, "", "", "collection1", Region.Tr);
437
- expect(resp5Y.previous_close).toBe(98.5);
438
- expect(client.getAggregateGraph).toHaveBeenCalledWith(HistoricalPricePeriod.FiveYear, "", "", "collection1", Region.Tr);
439
- });
440
- });
441
-
442
- describe("Error Handling", () => {
443
- test("should handle invalid industry ID", async () => {
444
- jest.spyOn(client, 'getIndustryDetail').mockRejectedValue(new Error("Industry not found"));
445
-
446
- await expect(client.getIndustryDetail("invalid-id", Region.Tr, Locale.Tr))
447
- .rejects.toThrow("Industry not found");
448
- });
449
-
450
- test("should handle invalid sector ID", async () => {
451
- jest.spyOn(client, 'getSectorDetail').mockRejectedValue(new Error("Sector not found"));
452
-
453
- await expect(client.getSectorDetail("invalid-id", Region.Tr, Locale.Tr))
454
- .rejects.toThrow("Sector not found");
455
- });
456
-
457
- test("should handle invalid theme ID", async () => {
458
- jest.spyOn(client, 'getThemeDetail').mockRejectedValue(new Error("Theme not found"));
459
-
460
- await expect(client.getThemeDetail("invalid-id", Region.Tr, Locale.Tr))
461
- .rejects.toThrow("Theme not found");
462
- });
463
-
464
- test("should handle invalid collection ID", async () => {
465
- jest.spyOn(client, 'getCollectionDetail').mockRejectedValue(new Error("Collection not found"));
466
-
467
- await expect(client.getCollectionDetail("invalid-id", Region.Tr, Locale.Tr))
468
- .rejects.toThrow("Collection not found");
679
+ expect(resp.graph[2].o).toBe(106.4);
680
+ expect(resp.graph[2].h).toBe(108.9);
681
+ expect(resp.graph[2].l).toBe(105.1);
682
+ expect(resp.graph[2].c).toBe(107.8);
469
683
  });
470
684
 
471
- test("should handle invalid aggregate graph request", async () => {
472
- jest.spyOn(client, 'getAggregateGraph').mockRejectedValue(new Error("Invalid parameters"));
473
-
474
- await expect(client.getAggregateGraph("invalid-period" as HistoricalPricePeriod, "invalid-sector", "", "", Region.Tr))
475
- .rejects.toThrow("Invalid parameters");
685
+ test("bubbles up request error", async () => {
686
+ cli.request.mockRejectedValueOnce(new Error("Invalid parameters"));
687
+
688
+ await expect(
689
+ client.getAggregateGraph(
690
+ AggregateGraphPeriod.OneMonth,
691
+ "",
692
+ "",
693
+ "collection1",
694
+ Region.Tr
695
+ )
696
+ ).rejects.toThrow("Invalid parameters");
476
697
  });
477
698
  });
478
699
  });