mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
3 changed files with 363 additions and 2 deletions
@ -0,0 +1,336 @@ |
|||||
|
import { AccountService } from '@ghostfolio/api/app/account/account.service'; |
||||
|
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; |
||||
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; |
||||
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; |
||||
|
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; |
||||
|
import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathering/data-gathering.service'; |
||||
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; |
||||
|
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; |
||||
|
import { UserWithSettings } from '@ghostfolio/common/types'; |
||||
|
|
||||
|
import { DataSource, SymbolProfile } from '@prisma/client'; |
||||
|
|
||||
|
import { ImportService } from './import.service'; |
||||
|
|
||||
|
let mockExistingAssetProfiles: Partial<SymbolProfile>[] = []; |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/account/account.service', () => { |
||||
|
return { |
||||
|
AccountService: jest.fn().mockImplementation(() => { |
||||
|
return { |
||||
|
getAccounts: () => Promise.resolve([]) |
||||
|
}; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/app/activities/activities.service', () => { |
||||
|
return { |
||||
|
ActivitiesService: jest.fn().mockImplementation(() => { |
||||
|
return { |
||||
|
getActivities: () => Promise.resolve({ activities: [] }), |
||||
|
createActivity: jest.fn().mockImplementation((data) => { |
||||
|
return Promise.resolve({ |
||||
|
id: 'ee3949fa-9df5-4b4e-9856-14dd1cfe9c86', |
||||
|
SymbolProfile: { |
||||
|
symbol: data.SymbolProfile.connectOrCreate.create.symbol |
||||
|
} |
||||
|
}); |
||||
|
}) |
||||
|
}; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/data-provider/data-provider.service', |
||||
|
() => { |
||||
|
return { |
||||
|
DataProviderService: jest.fn().mockImplementation(() => { |
||||
|
return { |
||||
|
getDataSourceForImport: () => DataSource.MANUAL, |
||||
|
validateActivities: () => Promise.resolve({}) |
||||
|
}; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service', |
||||
|
() => { |
||||
|
return { |
||||
|
ExchangeRateDataService: jest.fn().mockImplementation(() => { |
||||
|
return { |
||||
|
toCurrencyAtDate: () => Promise.resolve(0) |
||||
|
}; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => { |
||||
|
return { |
||||
|
MarketDataService: jest.fn().mockImplementation(() => { |
||||
|
return { |
||||
|
updateMany: () => Promise.resolve() |
||||
|
}; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/queues/data-gathering/data-gathering.service', |
||||
|
() => { |
||||
|
return { |
||||
|
DataGatheringService: jest.fn().mockImplementation(() => { |
||||
|
return { |
||||
|
gatherSymbols: () => undefined |
||||
|
}; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock( |
||||
|
'@ghostfolio/api/services/symbol-profile/symbol-profile.service', |
||||
|
() => { |
||||
|
return { |
||||
|
SymbolProfileService: jest.fn().mockImplementation(() => { |
||||
|
return { |
||||
|
add: jest.fn().mockResolvedValue(undefined), |
||||
|
getSymbolProfiles: () => Promise.resolve(mockExistingAssetProfiles) |
||||
|
}; |
||||
|
}) |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
jest.mock('@ghostfolio/api/services/tag/tag.service', () => { |
||||
|
return { |
||||
|
TagService: jest.fn().mockImplementation(() => { |
||||
|
return { |
||||
|
getTagsForUser: () => Promise.resolve([]) |
||||
|
}; |
||||
|
}) |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
describe('ImportService', () => { |
||||
|
let accountService: AccountService; |
||||
|
let activitiesService: ActivitiesService; |
||||
|
let dataGatheringService: DataGatheringService; |
||||
|
let dataProviderService: DataProviderService; |
||||
|
let exchangeRateDataService: ExchangeRateDataService; |
||||
|
let importService: ImportService; |
||||
|
let marketDataService: MarketDataService; |
||||
|
let symbolProfileService: SymbolProfileService; |
||||
|
let tagService: TagService; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
mockExistingAssetProfiles = []; |
||||
|
|
||||
|
accountService = new AccountService(null, null, null, null, null); |
||||
|
activitiesService = new ActivitiesService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
dataGatheringService = new DataGatheringService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
dataProviderService = new DataProviderService( |
||||
|
null, |
||||
|
[], |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
exchangeRateDataService = new ExchangeRateDataService( |
||||
|
null, |
||||
|
null, |
||||
|
null, |
||||
|
null |
||||
|
); |
||||
|
marketDataService = new MarketDataService(null); |
||||
|
symbolProfileService = new SymbolProfileService(null); |
||||
|
tagService = new TagService(null); |
||||
|
|
||||
|
importService = new ImportService( |
||||
|
accountService, |
||||
|
activitiesService, |
||||
|
null, |
||||
|
dataGatheringService, |
||||
|
dataProviderService, |
||||
|
exchangeRateDataService, |
||||
|
marketDataService, |
||||
|
null, |
||||
|
null, |
||||
|
symbolProfileService, |
||||
|
tagService |
||||
|
); |
||||
|
}); |
||||
|
|
||||
|
it('creates only one asset profile when the import payload contains duplicate new manual asset profiles', async () => { |
||||
|
const user = { |
||||
|
id: 'da8a5786-1223-4a51-9a86-2b60433c9f3f', |
||||
|
permissions: [], |
||||
|
settings: { settings: { baseCurrency: 'USD' } } |
||||
|
} as unknown as UserWithSettings; |
||||
|
|
||||
|
const assetProfile = { |
||||
|
currency: 'USD', |
||||
|
dataSource: DataSource.MANUAL, |
||||
|
isActive: true, |
||||
|
marketData: [], |
||||
|
name: 'Repeated Fee', |
||||
|
symbol: 'Repeated Fee' |
||||
|
}; |
||||
|
|
||||
|
await importService.import({ |
||||
|
accountsWithBalancesDto: [], |
||||
|
activitiesDto: [ |
||||
|
{ |
||||
|
currency: 'USD', |
||||
|
dataSource: DataSource.MANUAL, |
||||
|
date: '2024-01-01T00:00:00.000Z', |
||||
|
fee: 1, |
||||
|
quantity: 0, |
||||
|
symbol: 'Repeated Fee', |
||||
|
type: 'FEE', |
||||
|
unitPrice: 0 |
||||
|
}, |
||||
|
{ |
||||
|
currency: 'USD', |
||||
|
dataSource: DataSource.MANUAL, |
||||
|
date: '2024-01-02T00:00:00.000Z', |
||||
|
fee: 1, |
||||
|
quantity: 0, |
||||
|
symbol: 'Repeated Fee', |
||||
|
type: 'FEE', |
||||
|
unitPrice: 0 |
||||
|
} |
||||
|
], |
||||
|
assetProfilesWithMarketDataDto: [assetProfile, assetProfile], |
||||
|
maxActivitiesToImport: 10, |
||||
|
platformsDto: [], |
||||
|
tagsDto: [], |
||||
|
user |
||||
|
}); |
||||
|
|
||||
|
expect(symbolProfileService.add).toHaveBeenCalledTimes(1); |
||||
|
}); |
||||
|
|
||||
|
it('reuses an existing manual asset profile without a user', async () => { |
||||
|
mockExistingAssetProfiles = [ |
||||
|
{ |
||||
|
currency: 'USD', |
||||
|
dataSource: DataSource.MANUAL, |
||||
|
name: 'Manual Asset Profile', |
||||
|
symbol: 'GF_MANUAL', |
||||
|
userId: null |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
await importActivitiesWithExistingAssetProfile(); |
||||
|
|
||||
|
expect(symbolProfileService.add).not.toHaveBeenCalled(); |
||||
|
|
||||
|
for (const [{ SymbolProfile }] of ( |
||||
|
activitiesService.createActivity as jest.Mock |
||||
|
).mock.calls) { |
||||
|
expect(SymbolProfile.connectOrCreate.create.symbol).toEqual('GF_MANUAL'); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
it('creates a new asset profile when the existing manual asset profile belongs to a different user', async () => { |
||||
|
mockExistingAssetProfiles = [ |
||||
|
{ |
||||
|
currency: 'USD', |
||||
|
dataSource: DataSource.MANUAL, |
||||
|
name: 'Manual Asset Profile', |
||||
|
symbol: 'GF_MANUAL', |
||||
|
userId: '5b7a1b3a-1f1c-4c7a-9a1a-3a1b5b7a1b3a' |
||||
|
} |
||||
|
]; |
||||
|
|
||||
|
await importActivitiesWithExistingAssetProfile(); |
||||
|
|
||||
|
expect(symbolProfileService.add).toHaveBeenCalledTimes(1); |
||||
|
|
||||
|
const [[{ symbol }]] = (symbolProfileService.add as jest.Mock).mock.calls; |
||||
|
|
||||
|
expect(symbol).not.toEqual('GF_MANUAL'); |
||||
|
|
||||
|
for (const [{ SymbolProfile }] of ( |
||||
|
activitiesService.createActivity as jest.Mock |
||||
|
).mock.calls) { |
||||
|
expect(SymbolProfile.connectOrCreate.create.symbol).toEqual(symbol); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
function importActivitiesWithExistingAssetProfile() { |
||||
|
const user = { |
||||
|
id: 'da8a5786-1223-4a51-9a86-2b60433c9f3f', |
||||
|
permissions: [], |
||||
|
settings: { settings: { baseCurrency: 'USD' } } |
||||
|
} as unknown as UserWithSettings; |
||||
|
|
||||
|
// The client creates a synthetic asset profile per activity
|
||||
|
const assetProfile = { |
||||
|
currency: 'USD', |
||||
|
dataSource: DataSource.MANUAL, |
||||
|
isActive: true, |
||||
|
marketData: [], |
||||
|
name: 'GF_MANUAL', |
||||
|
symbol: 'GF_MANUAL' |
||||
|
}; |
||||
|
|
||||
|
return importService.import({ |
||||
|
accountsWithBalancesDto: [], |
||||
|
activitiesDto: [ |
||||
|
{ |
||||
|
currency: 'USD', |
||||
|
dataSource: DataSource.MANUAL, |
||||
|
date: '2024-01-01T00:00:00.000Z', |
||||
|
fee: 0, |
||||
|
quantity: 1, |
||||
|
symbol: 'GF_MANUAL', |
||||
|
type: 'BUY', |
||||
|
unitPrice: 1 |
||||
|
}, |
||||
|
{ |
||||
|
currency: 'USD', |
||||
|
dataSource: DataSource.MANUAL, |
||||
|
date: '2024-01-02T00:00:00.000Z', |
||||
|
fee: 0, |
||||
|
quantity: 2, |
||||
|
symbol: 'GF_MANUAL', |
||||
|
type: 'BUY', |
||||
|
unitPrice: 1 |
||||
|
} |
||||
|
], |
||||
|
assetProfilesWithMarketDataDto: [assetProfile, assetProfile], |
||||
|
maxActivitiesToImport: 10, |
||||
|
platformsDto: [], |
||||
|
tagsDto: [], |
||||
|
user |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
Loading…
Reference in new issue