mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
206 lines
5.9 KiB
206 lines
5.9 KiB
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: []
|
|
});
|
|
}
|
|
});
|
|
|