feat: implement update functionality for receipt aliases and add corresponding tests
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { IsInt, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class UpdateReceiptAliasDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
receiptName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
productId?: number;
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ReceiptAliasService } from './receipt-alias.service';
|
||||
import { CreateReceiptAliasDto } from './dto/create-receipt-alias.dto';
|
||||
import { UpdateReceiptAliasDto } from './dto/update-receipt-alias.dto';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
|
||||
@Controller('receipt-aliases')
|
||||
@@ -20,6 +30,15 @@ export class ReceiptAliasController {
|
||||
return this.receiptAliasService.upsert(dto, user.userId, user.role);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateReceiptAliasDto,
|
||||
@CurrentUser() user: { userId: number; role: string },
|
||||
) {
|
||||
return this.receiptAliasService.update(id, dto, user.userId, user.role);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
|
||||
@@ -4,6 +4,7 @@ describe('ReceiptAlias controller security', () => {
|
||||
const receiptAliasServiceMock = {
|
||||
findAllForUser: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
update: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -37,4 +38,13 @@ describe('ReceiptAlias controller security', () => {
|
||||
|
||||
expect(receiptAliasServiceMock.remove).toHaveBeenCalledWith(10, 42, 'user');
|
||||
});
|
||||
|
||||
it('update scopear till @CurrentUser userId + role', () => {
|
||||
const dto = { receiptName: 'Arla mjolk 1l', productId: 7 };
|
||||
receiptAliasServiceMock.update.mockResolvedValue({ id: 10 });
|
||||
|
||||
controller.update(10, dto as any, { userId: 42, role: 'user' });
|
||||
|
||||
expect(receiptAliasServiceMock.update).toHaveBeenCalledWith(10, dto, 42, 'user');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,4 +64,65 @@ describe('ReceiptAliasService', () => {
|
||||
|
||||
await expect(service.remove(99, 10, 'admin')).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('uppdaterar alias säkert via update()', async () => {
|
||||
prismaMock.receiptAlias.findUnique.mockResolvedValue({
|
||||
id: 12,
|
||||
receiptName: 'mjolk 1l',
|
||||
productId: 7,
|
||||
ownerId: 10,
|
||||
isGlobal: false,
|
||||
});
|
||||
prismaMock.receiptAlias.findFirst.mockResolvedValue(null);
|
||||
prismaMock.receiptAlias.update.mockResolvedValue({ id: 12 });
|
||||
|
||||
await service.update(
|
||||
12,
|
||||
{ receiptName: ' ARLA MJOLK 1L ', productId: 8 },
|
||||
10,
|
||||
'user',
|
||||
);
|
||||
|
||||
expect(prismaMock.receiptAlias.update).toHaveBeenCalledWith({
|
||||
where: { id: 12 },
|
||||
data: {
|
||||
receiptName: 'arla mjolk 1l',
|
||||
productId: 8,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('blockerar update när aliasnamn krockar i samma scope', async () => {
|
||||
prismaMock.receiptAlias.findUnique.mockResolvedValue({
|
||||
id: 12,
|
||||
receiptName: 'mjolk 1l',
|
||||
productId: 7,
|
||||
ownerId: 10,
|
||||
isGlobal: false,
|
||||
});
|
||||
prismaMock.receiptAlias.findFirst.mockResolvedValue({
|
||||
id: 99,
|
||||
receiptName: 'arla mjolk 1l',
|
||||
ownerId: 10,
|
||||
isGlobal: false,
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.update(12, { receiptName: 'ARLA MJOLK 1L' }, 10, 'user'),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('blockerar update för obehörig användare', async () => {
|
||||
prismaMock.receiptAlias.findUnique.mockResolvedValue({
|
||||
id: 12,
|
||||
receiptName: 'mjolk 1l',
|
||||
productId: 7,
|
||||
ownerId: null,
|
||||
isGlobal: true,
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.update(12, { receiptName: 'mjolk 1 liter' }, 10, 'user'),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateReceiptAliasDto } from './dto/create-receipt-alias.dto';
|
||||
import { UpdateReceiptAliasDto } from './dto/update-receipt-alias.dto';
|
||||
import {
|
||||
normalizeReceiptAliasName,
|
||||
validateReceiptAliasName,
|
||||
@@ -93,4 +94,60 @@ export class ReceiptAliasService {
|
||||
|
||||
return this.prisma.receiptAlias.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async update(
|
||||
id: number,
|
||||
dto: UpdateReceiptAliasDto,
|
||||
userId: number,
|
||||
role: string,
|
||||
) {
|
||||
const alias = await this.prisma.receiptAlias.findUnique({ where: { id } });
|
||||
if (!alias) {
|
||||
throw new NotFoundException(`Aliaspost med id ${id} hittades inte`);
|
||||
}
|
||||
|
||||
const canEdit =
|
||||
role === 'admin' ||
|
||||
(alias.ownerId === userId && alias.isGlobal === false);
|
||||
|
||||
if (!canEdit) {
|
||||
throw new ForbiddenException('Du har inte behörighet att redigera aliaset');
|
||||
}
|
||||
|
||||
if (dto.receiptName == null && dto.productId == null) {
|
||||
throw new BadRequestException('Inget att uppdatera. Ange receiptName eller productId.');
|
||||
}
|
||||
|
||||
let nextReceiptName = alias.receiptName;
|
||||
if (dto.receiptName != null) {
|
||||
nextReceiptName = normalizeReceiptAliasName(dto.receiptName);
|
||||
const validationError = validateReceiptAliasName(nextReceiptName);
|
||||
if (validationError) {
|
||||
throw new BadRequestException(validationError);
|
||||
}
|
||||
}
|
||||
|
||||
const nextProductId = dto.productId ?? alias.productId;
|
||||
|
||||
// Skydda mot krock i samma scope när receiptName ändras.
|
||||
if (nextReceiptName !== alias.receiptName) {
|
||||
const conflict = await this.prisma.receiptAlias.findFirst({
|
||||
where: alias.isGlobal
|
||||
? { receiptName: nextReceiptName, isGlobal: true }
|
||||
: { receiptName: nextReceiptName, ownerId: alias.ownerId, isGlobal: false },
|
||||
});
|
||||
|
||||
if (conflict && conflict.id !== alias.id) {
|
||||
throw new BadRequestException('Aliasnamnet finns redan i samma scope.');
|
||||
}
|
||||
}
|
||||
|
||||
return this.prisma.receiptAlias.update({
|
||||
where: { id: alias.id },
|
||||
data: {
|
||||
receiptName: nextReceiptName,
|
||||
productId: nextProductId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user