From 7db2bbe6a59df48168e68a623641e3f2aef8803c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:42:01 +0200 Subject: [PATCH] Bugfix/data provider mapping in get data provider (#8195) * Fix data source mapping in data provider service * Update changelog --- CHANGELOG.md | 4 + .../data-provider.service.spec.ts | 112 ++++++++++++++++++ .../data-provider/data-provider.service.ts | 36 +++--- 3 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 apps/api/src/services/data-provider/data-provider.service.spec.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b846c573d4..ef792cf2d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved the accessibility of the _Google_ icon on the registration page - Improved the search by ISIN number in the _Financial Modeling Prep_ service +### Fixed + +- Fixed the data source mapping in the data provider service to apply only to the mapped data source + ## 3.87.0 - 2026-10-17 ### Added diff --git a/apps/api/src/services/data-provider/data-provider.service.spec.ts b/apps/api/src/services/data-provider/data-provider.service.spec.ts new file mode 100644 index 0000000000..20f5acaadb --- /dev/null +++ b/apps/api/src/services/data-provider/data-provider.service.spec.ts @@ -0,0 +1,112 @@ +import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; +import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; +import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; +import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; +import { PropertyService } from '@ghostfolio/api/services/property/property.service'; + +import { DataSource } from '@prisma/client'; + +import { DataProviderService } from './data-provider.service'; + +/** + * Gives the service with some of the data providers in the order of the + * module, so that EOD_HISTORICAL_DATA comes before MANUAL and YAHOO + */ +function createDataProviderService(dataProviderMapping?: { + [dataProviderName: string]: string; +}) { + const dataProviderInterfaces = [ + DataSource.EOD_HISTORICAL_DATA, + DataSource.FINANCIAL_MODELING_PREP, + DataSource.MANUAL, + DataSource.YAHOO + ].map((dataSource) => { + return { + getName: () => { + return dataSource; + } + } as DataProviderInterface; + }); + + return new DataProviderService( + {} as ConfigurationService, + dataProviderInterfaces, + {} as MarketDataService, + {} as PrismaService, + { + getByKey: jest.fn().mockResolvedValue(dataProviderMapping) + } as unknown as PropertyService, + {} as RedisCacheService + ); +} + +describe('DataProviderService', () => { + describe('getDataProvider', () => { + it('gives the data provider of the data source without a mapping', async () => { + const dataProviderService = createDataProviderService(); + + await dataProviderService.onModuleInit(); + + expect( + dataProviderService.getDataProvider(DataSource.YAHOO).getName() + ).toBe(DataSource.YAHOO); + }); + + it('gives the mapped data provider of a data source with a mapping', async () => { + const dataProviderService = createDataProviderService({ + [DataSource.YAHOO]: DataSource.FINANCIAL_MODELING_PREP + }); + + await dataProviderService.onModuleInit(); + + expect( + dataProviderService.getDataProvider(DataSource.YAHOO).getName() + ).toBe(DataSource.FINANCIAL_MODELING_PREP); + }); + + it('gives the data provider of the data source if the mapping of another data source comes first', async () => { + const dataProviderService = createDataProviderService({ + [DataSource.EOD_HISTORICAL_DATA]: DataSource.FINANCIAL_MODELING_PREP + }); + + await dataProviderService.onModuleInit(); + + expect( + dataProviderService + .getDataProvider(DataSource.EOD_HISTORICAL_DATA) + .getName() + ).toBe(DataSource.FINANCIAL_MODELING_PREP); + + expect( + dataProviderService.getDataProvider(DataSource.MANUAL).getName() + ).toBe(DataSource.MANUAL); + + expect( + dataProviderService.getDataProvider(DataSource.YAHOO).getName() + ).toBe(DataSource.YAHOO); + }); + + it('gives the data provider of the data source if the mapped data provider does not exist', async () => { + const dataProviderService = createDataProviderService({ + [DataSource.YAHOO]: 'UNKNOWN' + }); + + await dataProviderService.onModuleInit(); + + expect( + dataProviderService.getDataProvider(DataSource.YAHOO).getName() + ).toBe(DataSource.YAHOO); + }); + + it('throws an error if no data provider has been found', async () => { + const dataProviderService = createDataProviderService(); + + await dataProviderService.onModuleInit(); + + expect(() => { + return dataProviderService.getDataProvider(DataSource.COINGECKO); + }).toThrow('No data provider has been found.'); + }); + }); +}); diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 914bb65427..3206b2442b 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -157,26 +157,32 @@ export class DataProviderService implements OnModuleInit { return response; } + /** + * Gives the data provider of the data source, or the data provider which + * the data source mapping gives for this data source + */ public getDataProvider(providerName: DataSource) { - for (const dataProviderInterface of this.dataProviderInterfaces) { - if (this.dataProviderMapping[dataProviderInterface.getName()]) { - const mappedDataProviderInterface = this.dataProviderInterfaces.find( - (currentDataProviderInterface) => { - return ( - currentDataProviderInterface.getName() === - this.dataProviderMapping[dataProviderInterface.getName()] - ); - } + const mappedDataProviderInterface = this.dataProviderInterfaces.find( + (dataProviderInterface) => { + return ( + dataProviderInterface.getName() === + this.dataProviderMapping[providerName] ); - - if (mappedDataProviderInterface) { - return mappedDataProviderInterface; - } } + ); + + if (mappedDataProviderInterface) { + return mappedDataProviderInterface; + } - if (dataProviderInterface.getName() === providerName) { - return dataProviderInterface; + const dataProviderInterface = this.dataProviderInterfaces.find( + (currentDataProviderInterface) => { + return currentDataProviderInterface.getName() === providerName; } + ); + + if (dataProviderInterface) { + return dataProviderInterface; } throw new Error('No data provider has been found.');