mirror of https://github.com/ghostfolio/ghostfolio
committed by
GitHub
2 changed files with 376 additions and 3 deletions
@ -0,0 +1,206 @@ |
|||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; |
|||
import { UserWithSettings } from '@ghostfolio/common/types'; |
|||
|
|||
import { DataSource, Prisma } from '@prisma/client'; |
|||
|
|||
import { ImportService } from './import.service'; |
|||
|
|||
describe('ImportService', () => { |
|||
const accountService = { |
|||
getAccounts: jest.fn() |
|||
}; |
|||
const activitiesService = { |
|||
createActivity: jest.fn(), |
|||
getActivities: jest.fn() |
|||
}; |
|||
const apiService = {}; |
|||
const dataGatheringService = { |
|||
gatherSymbols: jest.fn() |
|||
}; |
|||
const dataProviderService = { |
|||
validateActivities: jest.fn() |
|||
}; |
|||
const exchangeRateDataService = { |
|||
toCurrencyAtDate: jest.fn() |
|||
}; |
|||
const marketDataService = {}; |
|||
const platformService = {}; |
|||
const portfolioService = {}; |
|||
const symbolProfileService = { |
|||
add: jest.fn(), |
|||
getSymbolProfiles: jest.fn() |
|||
}; |
|||
const tagService = { |
|||
getTagsForUser: jest.fn() |
|||
}; |
|||
const user = { |
|||
id: 'user-1', |
|||
permissions: [], |
|||
settings: { |
|||
settings: { |
|||
baseCurrency: 'USD' |
|||
} |
|||
} |
|||
} as unknown as UserWithSettings; |
|||
|
|||
let importService: ImportService; |
|||
|
|||
beforeEach(() => { |
|||
jest.resetAllMocks(); |
|||
|
|||
accountService.getAccounts.mockResolvedValue([]); |
|||
activitiesService.getActivities.mockResolvedValue({ activities: [] }); |
|||
activitiesService.createActivity.mockImplementation(async (data) => { |
|||
const assetProfile = data.SymbolProfile.connectOrCreate.create; |
|||
|
|||
return { |
|||
SymbolProfile: assetProfile, |
|||
date: data.date, |
|||
id: `activity-${data.date.toISOString()}`, |
|||
userId: user.id |
|||
}; |
|||
}); |
|||
exchangeRateDataService.toCurrencyAtDate.mockResolvedValue(1); |
|||
tagService.getTagsForUser.mockResolvedValue([]); |
|||
|
|||
importService = new ImportService( |
|||
accountService as never, |
|||
activitiesService as never, |
|||
apiService as never, |
|||
dataGatheringService as never, |
|||
dataProviderService as never, |
|||
exchangeRateDataService as never, |
|||
marketDataService as never, |
|||
platformService as never, |
|||
portfolioService as never, |
|||
symbolProfileService as never, |
|||
tagService as never |
|||
); |
|||
}); |
|||
|
|||
it('creates a new symbol profile once for duplicate activities', async () => { |
|||
const symbolProfile = createSymbolProfile(); |
|||
symbolProfileService.getSymbolProfiles.mockResolvedValue([]); |
|||
symbolProfileService.add.mockResolvedValue(symbolProfile); |
|||
|
|||
const activities = await importDuplicateActivities(symbolProfile); |
|||
|
|||
expect(symbolProfileService.add).toHaveBeenCalledTimes(1); |
|||
expect(activitiesService.createActivity).toHaveBeenCalledTimes(2); |
|||
expect(activities).toHaveLength(2); |
|||
}); |
|||
|
|||
it('reuses an existing symbol profile without creating it', async () => { |
|||
const symbolProfile = createSymbolProfile(); |
|||
symbolProfileService.getSymbolProfiles.mockResolvedValue([symbolProfile]); |
|||
|
|||
const activities = await importDuplicateActivities(symbolProfile); |
|||
|
|||
expect(symbolProfileService.add).not.toHaveBeenCalled(); |
|||
expect(activitiesService.createActivity).toHaveBeenCalledTimes(2); |
|||
expect(activities).toHaveLength(2); |
|||
}); |
|||
|
|||
it('refetches the canonical profile after a matching P2002 conflict', async () => { |
|||
const symbolProfile = createSymbolProfile(); |
|||
const conflict = new Prisma.PrismaClientKnownRequestError( |
|||
'Unique constraint failed', |
|||
{ |
|||
clientVersion: 'test', |
|||
code: 'P2002', |
|||
meta: { |
|||
driverAdapterError: { |
|||
cause: { |
|||
constraint: { |
|||
fields: ['"dataSource"', 'symbol'] |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
); |
|||
symbolProfileService.getSymbolProfiles |
|||
.mockResolvedValueOnce([]) |
|||
.mockResolvedValueOnce([symbolProfile]); |
|||
symbolProfileService.add.mockRejectedValue(conflict); |
|||
|
|||
const activities = await importDuplicateActivities(symbolProfile); |
|||
|
|||
expect(symbolProfileService.getSymbolProfiles).toHaveBeenCalledTimes(2); |
|||
expect(activitiesService.createActivity).toHaveBeenCalledTimes(2); |
|||
expect(activities).toHaveLength(2); |
|||
}); |
|||
|
|||
it('rethrows an unrelated P2002 conflict', async () => { |
|||
const symbolProfile = createSymbolProfile(); |
|||
const conflict = new Prisma.PrismaClientKnownRequestError( |
|||
'Unique constraint failed', |
|||
{ |
|||
clientVersion: 'test', |
|||
code: 'P2002', |
|||
meta: { |
|||
target: ['id'] |
|||
} |
|||
} |
|||
); |
|||
symbolProfileService.getSymbolProfiles.mockResolvedValue([]); |
|||
symbolProfileService.add.mockRejectedValue(conflict); |
|||
|
|||
await expect(importDuplicateActivities(symbolProfile)).rejects.toBe( |
|||
conflict |
|||
); |
|||
expect(activitiesService.createActivity).not.toHaveBeenCalled(); |
|||
}); |
|||
|
|||
function createSymbolProfile() { |
|||
return { |
|||
currency: 'USD', |
|||
dataSource: DataSource.MANUAL, |
|||
name: 'Repeated fee', |
|||
symbol: 'GF_REPEATED_FEE' |
|||
}; |
|||
} |
|||
|
|||
async function importDuplicateActivities( |
|||
symbolProfile: ReturnType<typeof createSymbolProfile> |
|||
) { |
|||
const activitiesDto = [ |
|||
{ |
|||
currency: symbolProfile.currency, |
|||
dataSource: symbolProfile.dataSource, |
|||
date: '2026-01-01T00:00:00.000Z', |
|||
fee: 0, |
|||
quantity: 1, |
|||
symbol: symbolProfile.symbol, |
|||
tags: [], |
|||
type: 'FEE' as const, |
|||
unitPrice: 1 |
|||
}, |
|||
{ |
|||
currency: symbolProfile.currency, |
|||
dataSource: symbolProfile.dataSource, |
|||
date: '2026-01-02T00:00:00.000Z', |
|||
fee: 0, |
|||
quantity: 1, |
|||
symbol: symbolProfile.symbol, |
|||
tags: [], |
|||
type: 'FEE' as const, |
|||
unitPrice: 1 |
|||
} |
|||
]; |
|||
const assetProfileIdentifier = getAssetProfileIdentifier(symbolProfile); |
|||
dataProviderService.validateActivities.mockResolvedValue({ |
|||
[assetProfileIdentifier]: symbolProfile |
|||
}); |
|||
|
|||
return importService.import({ |
|||
activitiesDto, |
|||
maxActivitiesToImport: 10, |
|||
user, |
|||
accountsWithBalancesDto: [], |
|||
assetProfilesWithMarketDataDto: [], |
|||
platformsDto: [], |
|||
tagsDto: [] |
|||
}); |
|||
} |
|||
}); |
|||
Loading…
Reference in new issue