feat: add unit tests for ReceiptImportService.saveReceipt method
Test Suite / backend-pr-quick (24.15.0) (push) Has been skipped
Test Suite / backend-full (24.15.0) (push) Failing after 21s
Test Suite / flutter-quality (push) Failing after 4s

This commit is contained in:
Nils-Johan Gynther
2026-05-12 20:01:30 +02:00
parent 320a646950
commit 44ea3cdd7e
@@ -0,0 +1,209 @@
import { BadRequestException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { ReceiptImportService } from './receipt-import.service';
import { SaveReceiptDto } from './dto/save-receipt.dto';
// ──────────────────────────────────────────────────────────────────────────────
// Hjälpfunktioner
// ──────────────────────────────────────────────────────────────────────────────
function makeItem(overrides: Partial<SaveReceiptDto['items'][0]> = {}): SaveReceiptDto['items'][0] {
return {
rawName: 'ARLA MJOLK 1L',
quantity: 2,
unit: 'st',
destination: 'inventory',
productId: 100,
...overrides,
} as SaveReceiptDto['items'][0];
}
// ──────────────────────────────────────────────────────────────────────────────
// Spec
// ──────────────────────────────────────────────────────────────────────────────
describe('ReceiptImportService.saveReceipt', () => {
let txMock: {
product: { findUnique: jest.Mock; create: jest.Mock; update: jest.Mock };
inventoryItem: { create: jest.Mock; update: jest.Mock };
pantryItem: { create: jest.Mock };
receiptAlias: { upsert: jest.Mock };
unitMapping: { upsert: jest.Mock };
};
let prismaMock: {
pantryItem: { findMany: jest.Mock };
inventoryItem: { findMany: jest.Mock };
$transaction: jest.Mock;
};
let service: ReceiptImportService;
beforeEach(() => {
txMock = {
product: {
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
inventoryItem: {
create: jest.fn().mockResolvedValue({ id: 1 }),
update: jest.fn().mockResolvedValue({ id: 1 }),
},
pantryItem: {
create: jest.fn().mockResolvedValue({ id: 1 }),
},
receiptAlias: {
upsert: jest.fn().mockResolvedValue({ id: 1 }),
},
unitMapping: {
upsert: jest.fn().mockResolvedValue({ id: 1 }),
},
};
prismaMock = {
pantryItem: { findMany: jest.fn().mockResolvedValue([]) },
inventoryItem: { findMany: jest.fn().mockResolvedValue([]) },
$transaction: jest.fn().mockImplementation(async (cb: (tx: typeof txMock) => Promise<void>) => cb(txMock)),
};
service = new ReceiptImportService(
prismaMock as any,
{} as any, // aiService används ej i saveReceipt
{} as any, // categoriesService används ej i saveReceipt
);
});
// ── 1. Skapar ny inventariepost ─────────────────────────────────────────────
it('skapar ny inventariepost när produkten finns och inte finns i inventariet', async () => {
txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });
const dto: SaveReceiptDto = { items: [makeItem()] };
const result = await service.saveReceipt(1, dto);
expect(txMock.inventoryItem.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ productId: 100, userId: 1 }),
}),
);
expect(result.created).toBe(1);
expect(result.merged).toBe(0);
expect(result.errors).toHaveLength(0);
});
// ── 2. Slår samman mängd när produkten redan finns i inventariet ────────────
it('slår samman mängd när produkten redan finns i inventariet', async () => {
prismaMock.inventoryItem.findMany.mockResolvedValue([
{ id: 50, productId: 100, quantity: new Prisma.Decimal(3), unit: 'st' },
]);
txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });
const dto: SaveReceiptDto = { items: [makeItem({ quantity: 2 })] };
const result = await service.saveReceipt(1, dto);
expect(txMock.inventoryItem.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 50 },
data: { quantity: { increment: new Prisma.Decimal(2) } },
}),
);
expect(result.merged).toBe(1);
expect(result.created).toBe(0);
});
// ── 3. Lägger till i skafferi ───────────────────────────────────────────────
it('lägger till i skafferi när destination är pantry och produkten inte finns där', async () => {
txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });
const dto: SaveReceiptDto = { items: [makeItem({ destination: 'pantry' })] };
const result = await service.saveReceipt(1, dto);
expect(txMock.pantryItem.create).toHaveBeenCalledWith(
expect.objectContaining({ data: { userId: 1, productId: 100 } }),
);
expect(result.pantryAdded).toBe(1);
expect(result.pantrySkipped).toBe(0);
});
// ── 4. Hoppar över skafferidublett ──────────────────────────────────────────
it('hoppar över produkt som redan finns i skafferiet', async () => {
prismaMock.pantryItem.findMany.mockResolvedValue([{ productId: 100 }]);
txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });
const dto: SaveReceiptDto = { items: [makeItem({ destination: 'pantry' })] };
const result = await service.saveReceipt(1, dto);
expect(txMock.pantryItem.create).not.toHaveBeenCalled();
expect(result.pantrySkipped).toBe(1);
expect(result.pantryAdded).toBe(0);
});
// ── 5. Lär in alias ─────────────────────────────────────────────────────────
it('lär in alias när learnAlias är true', async () => {
txMock.product.findUnique.mockResolvedValue({ id: 100, isActive: true });
const dto: SaveReceiptDto = {
items: [makeItem({ learnAlias: true, learnAliasGlobally: false })],
};
const result = await service.saveReceipt(1, dto);
expect(txMock.receiptAlias.upsert).toHaveBeenCalledWith(
expect.objectContaining({
create: expect.objectContaining({
receiptName: 'arla mjolk 1l',
productId: 100,
isGlobal: false,
ownerId: 1,
}),
}),
);
expect(result.aliasesLearned).toBe(1);
});
// ── 6. Skapar ny privat produkt via createProductName ───────────────────────
it('skapar ny produkt när createProductName anges och produkten inte finns', async () => {
txMock.product.findUnique.mockResolvedValue(null); // ingen befintlig
txMock.product.create.mockResolvedValue({ id: 200 });
const dto: SaveReceiptDto = {
items: [
makeItem({ productId: undefined, createProductName: 'Hemmagjord Senap' }),
],
};
const result = await service.saveReceipt(1, dto);
expect(txMock.product.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
name: 'Hemmagjord Senap',
isPrivate: true,
ownerId: 1,
}),
}),
);
expect(result.created).toBe(1);
expect(result.errors).toHaveLength(0);
});
// ── 7. Produkt hittades inte registrerar fel utan att kasta ───────────────
it('registrerar fel för okänt productId utan att kasta exception', async () => {
txMock.product.findUnique.mockResolvedValue(null);
const dto: SaveReceiptDto = { items: [makeItem({ productId: 999 })] };
const result = await service.saveReceipt(1, dto);
expect(result.errors).toHaveLength(1);
expect(result.errors![0].index).toBe(0);
expect(result.created).toBe(0);
});
// ── 8. Kastar BadRequestException om transaktionen misslyckas ───────────────
it('kastar BadRequestException om $transaction kastar', async () => {
prismaMock.$transaction.mockRejectedValue(new Error('DB krasch'));
const dto: SaveReceiptDto = { items: [makeItem()] };
await expect(service.saveReceipt(1, dto)).rejects.toThrow(BadRequestException);
});
});