Browse Source

Bugfix/update of asset profiles not owned by user (#8071)

* Fix update of asset profiles not owned by user

* Update changelog
pull/8147/head
Thomas Kaul 3 days ago
committed by GitHub
parent
commit
9345de0401
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 101
      apps/api/src/app/activities/activities.service.spec.ts
  3. 18
      apps/api/src/app/activities/activities.service.ts

1
CHANGELOG.md

@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the border color of the drop area in the import activities dialog (dark mode)
- Fixed the matching of assets and accounts in the import of activities with _Ghostfolio AI_ (experimental)
- Fixed an issue where the update of an activity could change the asset class and asset sub class of an asset profile which does not belong to the user
## 3.84.2 - 2026-10-11

101
apps/api/src/app/activities/activities.service.spec.ts

@ -18,7 +18,9 @@ import { Activity, Filter } from '@ghostfolio/common/interfaces';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
AssetClass,
AssetProfileSplit,
AssetSubClass,
DataSource,
Prisma,
Type as ActivityType
@ -519,6 +521,105 @@ describe('ActivitiesService', () => {
});
});
});
describe('updateActivity', () => {
it('updates the custom asset profile of the user', async () => {
const data = await getUpdatedActivityData({ userId: 'user-id' });
expect(data.SymbolProfile).toEqual({
update: {
data: {
assetClass: AssetClass.COMMODITY,
assetSubClass: AssetSubClass.PRECIOUS_METAL
},
where: { userId: 'user-id' }
}
});
});
it.each([
{ owner: 'the admin', userId: null },
{ owner: 'another user', userId: 'other-user-id' }
])('does not update the asset profile of $owner', async ({ userId }) => {
const data = await getUpdatedActivityData({ userId });
expect(data).not.toHaveProperty('SymbolProfile');
});
it('does not update the asset profile of a data provider', async () => {
const data = await getUpdatedActivityData({
dataSource: DataSource.YAHOO,
symbol: 'AAPL',
type: ActivityType.FEE,
userId: null
});
expect(data).not.toHaveProperty('SymbolProfile');
});
async function getUpdatedActivityData({
dataSource = DataSource.MANUAL,
symbol = 'GF_GOLD',
type = ActivityType.BUY,
userId
}: {
dataSource?: DataSource;
symbol?: string;
type?: ActivityType;
userId: string | null;
}) {
const update = jest.fn().mockResolvedValue({ userId: 'user-id' });
const service = new ActivitiesService(
null,
null,
null,
null,
null,
null,
{ emit: jest.fn() } as unknown as EventEmitter2,
null,
null,
{
order: {
update,
findUnique: jest
.fn()
.mockResolvedValue({ SymbolProfile: { userId } })
}
} as unknown as PrismaService,
null,
{ validateTagIds: jest.fn() } as unknown as TagService
);
await service.updateActivity({
data: {
type,
assetClass: AssetClass.COMMODITY,
assetSubClass: AssetSubClass.PRECIOUS_METAL,
date: parseDate('2024-01-01'),
SymbolProfile: {
connect: {
dataSource_symbol: {
dataSource,
symbol
}
},
update: {
assetClass: AssetClass.COMMODITY,
assetSubClass: AssetSubClass.PRECIOUS_METAL,
name: symbol
}
}
},
originalDate: parseDate('2024-01-01'),
userId: 'user-id',
where: { id: 'activity-id' }
});
return (update.mock.calls[0] as [Prisma.OrderUpdateArgs])[0].data;
}
});
});
function createActivity({

18
apps/api/src/app/activities/activities.service.ts

@ -933,7 +933,23 @@ export class ActivitiesService {
data.type === 'BUY')
) {
delete data.SymbolProfile.connect;
delete data.SymbolProfile.update.name;
const activityToUpdate = await this.prismaService.order.findUnique({
select: { SymbolProfile: { select: { userId: true } } },
where
});
if (activityToUpdate?.SymbolProfile.userId === userId) {
data.SymbolProfile.update = {
data: {
assetClass: data.assetClass,
assetSubClass: data.assetSubClass
},
where: { userId }
};
} else {
delete data.SymbolProfile;
}
} else {
delete data.SymbolProfile.update;

Loading…
Cancel
Save