feat: add unit tests for ReceiptImportService.saveReceipt method
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user