All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m30s
637 lines
22 KiB
TypeScript
637 lines
22 KiB
TypeScript
// src/tests/integration/receipt.integration.test.ts
|
|
/**
|
|
* Integration tests for Receipt processing workflow.
|
|
* Tests the complete flow from receipt upload to item extraction and inventory addition.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
import supertest from 'supertest';
|
|
import type { UserProfile } from '../../types';
|
|
import { createAndLoginUser } from '../utils/testHelpers';
|
|
import { cleanupDb } from '../utils/cleanup';
|
|
import { getPool } from '../../services/db/connection.db';
|
|
|
|
/**
|
|
* @vitest-environment node
|
|
*/
|
|
|
|
// Mock Bull Board to prevent BullMQAdapter from validating queue instances
|
|
vi.mock('@bull-board/api', () => ({
|
|
createBullBoard: vi.fn(),
|
|
}));
|
|
vi.mock('@bull-board/api/bullMQAdapter', () => ({
|
|
BullMQAdapter: vi.fn(),
|
|
}));
|
|
|
|
// Mock the queues to prevent actual background processing
|
|
// IMPORTANT: Must include all queue exports that are imported by workers.server.ts
|
|
vi.mock('../../services/queues.server', () => ({
|
|
receiptQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-job-id' }),
|
|
},
|
|
cleanupQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-cleanup-job-id' }),
|
|
},
|
|
flyerQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-flyer-job-id' }),
|
|
},
|
|
emailQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-email-job-id' }),
|
|
},
|
|
analyticsQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-analytics-job-id' }),
|
|
},
|
|
weeklyAnalyticsQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-weekly-analytics-job-id' }),
|
|
},
|
|
tokenCleanupQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-token-cleanup-job-id' }),
|
|
},
|
|
expiryAlertQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-expiry-alert-job-id' }),
|
|
},
|
|
barcodeDetectionQueue: {
|
|
add: vi.fn().mockResolvedValue({ id: 'mock-barcode-job-id' }),
|
|
},
|
|
}));
|
|
|
|
describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
|
let request: ReturnType<typeof supertest>;
|
|
let authToken = '';
|
|
let testUser: UserProfile;
|
|
const createdUserIds: string[] = [];
|
|
const createdReceiptIds: number[] = [];
|
|
const createdInventoryIds: number[] = [];
|
|
|
|
beforeAll(async () => {
|
|
vi.stubEnv('FRONTEND_URL', 'https://example.com');
|
|
const app = (await import('../../../server')).default;
|
|
request = supertest(app);
|
|
|
|
// Create a user for receipt tests
|
|
const { user, token } = await createAndLoginUser({
|
|
email: `receipt-test-user-${Date.now()}@example.com`,
|
|
fullName: 'Receipt Test User',
|
|
request,
|
|
});
|
|
testUser = user;
|
|
authToken = token;
|
|
createdUserIds.push(user.user.user_id);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
vi.unstubAllEnvs();
|
|
|
|
const pool = getPool();
|
|
|
|
// Clean up inventory items
|
|
if (createdInventoryIds.length > 0) {
|
|
await pool.query('DELETE FROM public.user_inventory WHERE inventory_id = ANY($1::int[])', [
|
|
createdInventoryIds,
|
|
]);
|
|
}
|
|
|
|
// Clean up receipt items and receipts
|
|
if (createdReceiptIds.length > 0) {
|
|
await pool.query('DELETE FROM public.receipt_items WHERE receipt_id = ANY($1::int[])', [
|
|
createdReceiptIds,
|
|
]);
|
|
await pool.query(
|
|
'DELETE FROM public.receipt_processing_log WHERE receipt_id = ANY($1::int[])',
|
|
[createdReceiptIds],
|
|
);
|
|
await pool.query('DELETE FROM public.receipts WHERE receipt_id = ANY($1::int[])', [
|
|
createdReceiptIds,
|
|
]);
|
|
}
|
|
|
|
await cleanupDb({ userIds: createdUserIds });
|
|
});
|
|
|
|
describe('POST /api/receipts - Upload Receipt', () => {
|
|
it('should upload a receipt image successfully', async () => {
|
|
// Create a simple test image buffer
|
|
const testImageBuffer = Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
'base64',
|
|
);
|
|
|
|
const response = await request
|
|
.post('/api/receipts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.attach('receipt', testImageBuffer, 'test-receipt.png')
|
|
.field('store_id', '1')
|
|
.field('transaction_date', '2024-01-15');
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.receipt_id).toBeDefined();
|
|
expect(response.body.data.job_id).toBe('mock-job-id');
|
|
|
|
createdReceiptIds.push(response.body.data.receipt_id);
|
|
});
|
|
|
|
it('should upload receipt without optional fields', async () => {
|
|
const testImageBuffer = Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
'base64',
|
|
);
|
|
|
|
const response = await request
|
|
.post('/api/receipts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.attach('receipt', testImageBuffer, 'test-receipt-2.png');
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.data.receipt_id).toBeDefined();
|
|
|
|
createdReceiptIds.push(response.body.data.receipt_id);
|
|
});
|
|
|
|
it('should reject request without file', async () => {
|
|
const response = await request
|
|
.post('/api/receipts')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
|
|
it('should reject unauthenticated requests', async () => {
|
|
const testImageBuffer = Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
'base64',
|
|
);
|
|
|
|
const response = await request
|
|
.post('/api/receipts')
|
|
.attach('receipt', testImageBuffer, 'test-receipt.png');
|
|
|
|
expect(response.status).toBe(401);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/receipts - List Receipts', () => {
|
|
beforeAll(async () => {
|
|
// Create some receipts for testing
|
|
const pool = getPool();
|
|
for (let i = 0; i < 3; i++) {
|
|
const result = await pool.query(
|
|
`INSERT INTO public.receipts (user_id, receipt_image_url, status)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING receipt_id`,
|
|
[
|
|
testUser.user.user_id,
|
|
`/uploads/receipts/test-${i}.jpg`,
|
|
i === 0 ? 'completed' : 'pending',
|
|
],
|
|
);
|
|
createdReceiptIds.push(result.rows[0].receipt_id);
|
|
}
|
|
});
|
|
|
|
it('should return paginated list of receipts', async () => {
|
|
const response = await request
|
|
.get('/api/receipts')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.receipts).toBeDefined();
|
|
expect(Array.isArray(response.body.data.receipts)).toBe(true);
|
|
expect(response.body.data.total).toBeGreaterThanOrEqual(3);
|
|
});
|
|
|
|
it('should support status filter', async () => {
|
|
const response = await request
|
|
.get('/api/receipts')
|
|
.query({ status: 'completed' })
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
response.body.data.receipts.forEach((receipt: { status: string }) => {
|
|
expect(receipt.status).toBe('completed');
|
|
});
|
|
});
|
|
|
|
it('should support pagination', async () => {
|
|
const response = await request
|
|
.get('/api/receipts')
|
|
.query({ limit: 2, offset: 0 })
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.receipts.length).toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
it('should only return receipts for the authenticated user', async () => {
|
|
// Create another user
|
|
const { user: otherUser, token: otherToken } = await createAndLoginUser({
|
|
email: `other-receipt-user-${Date.now()}@example.com`,
|
|
fullName: 'Other Receipt User',
|
|
request,
|
|
});
|
|
createdUserIds.push(otherUser.user.user_id);
|
|
|
|
const response = await request
|
|
.get('/api/receipts')
|
|
.set('Authorization', `Bearer ${otherToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
// Other user should have no receipts
|
|
expect(response.body.data.total).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/receipts/:receiptId - Get Receipt Details', () => {
|
|
let testReceiptId: number;
|
|
|
|
beforeAll(async () => {
|
|
const pool = getPool();
|
|
|
|
// First create or get a test store
|
|
const storeResult = await pool.query(
|
|
`INSERT INTO public.stores (name)
|
|
VALUES ('Test Store')
|
|
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
|
|
RETURNING store_id`,
|
|
);
|
|
const storeId = storeResult.rows[0].store_id;
|
|
|
|
const result = await pool.query(
|
|
`INSERT INTO public.receipts (user_id, receipt_image_url, status, store_id, total_amount_cents)
|
|
VALUES ($1, $2, 'completed', $3, 9999)
|
|
RETURNING receipt_id`,
|
|
[testUser.user.user_id, '/uploads/receipts/detail-test.jpg', storeId],
|
|
);
|
|
testReceiptId = result.rows[0].receipt_id;
|
|
createdReceiptIds.push(testReceiptId);
|
|
|
|
// Add some items to the receipt
|
|
await pool.query(
|
|
`INSERT INTO public.receipt_items (receipt_id, raw_item_description, quantity, price_paid_cents, status)
|
|
VALUES ($1, 'MILK 2% 4L', 1, 599, 'matched'),
|
|
($1, 'BREAD WHITE', 2, 498, 'unmatched')`,
|
|
[testReceiptId],
|
|
);
|
|
});
|
|
|
|
it('should return receipt with items', async () => {
|
|
const response = await request
|
|
.get(`/api/receipts/${testReceiptId}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.receipt).toBeDefined();
|
|
expect(response.body.data.receipt.receipt_id).toBe(testReceiptId);
|
|
expect(response.body.data.receipt.store_id).toBeDefined();
|
|
expect(response.body.data.items).toBeDefined();
|
|
expect(response.body.data.items.length).toBe(2);
|
|
});
|
|
|
|
it('should return 404 for non-existent receipt', async () => {
|
|
const response = await request
|
|
.get('/api/receipts/999999')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it("should not allow accessing another user's receipt", async () => {
|
|
const { user: otherUser, token: otherToken } = await createAndLoginUser({
|
|
email: `receipt-access-test-${Date.now()}@example.com`,
|
|
fullName: 'Receipt Access Test',
|
|
request,
|
|
});
|
|
createdUserIds.push(otherUser.user.user_id);
|
|
|
|
const response = await request
|
|
.get(`/api/receipts/${testReceiptId}`)
|
|
.set('Authorization', `Bearer ${otherToken}`);
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('DELETE /api/receipts/:receiptId - Delete Receipt', () => {
|
|
it('should delete a receipt', async () => {
|
|
// Create a receipt to delete
|
|
const pool = getPool();
|
|
const result = await pool.query(
|
|
`INSERT INTO public.receipts (user_id, receipt_image_url, status)
|
|
VALUES ($1, '/uploads/receipts/delete-test.jpg', 'pending')
|
|
RETURNING receipt_id`,
|
|
[testUser.user.user_id],
|
|
);
|
|
const receiptId = result.rows[0].receipt_id;
|
|
|
|
const response = await request
|
|
.delete(`/api/receipts/${receiptId}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(204);
|
|
|
|
// Verify deletion
|
|
const verifyResponse = await request
|
|
.get(`/api/receipts/${receiptId}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(verifyResponse.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/receipts/:receiptId/reprocess - Reprocess Receipt', () => {
|
|
let failedReceiptId: number;
|
|
|
|
beforeAll(async () => {
|
|
const pool = getPool();
|
|
const result = await pool.query(
|
|
`INSERT INTO public.receipts (user_id, receipt_image_url, status, error_details)
|
|
VALUES ($1, '/uploads/receipts/failed-test.jpg', 'failed', '{"message": "OCR failed"}'::jsonb)
|
|
RETURNING receipt_id`,
|
|
[testUser.user.user_id],
|
|
);
|
|
failedReceiptId = result.rows[0].receipt_id;
|
|
createdReceiptIds.push(failedReceiptId);
|
|
});
|
|
|
|
it('should queue a failed receipt for reprocessing', async () => {
|
|
const response = await request
|
|
.post(`/api/receipts/${failedReceiptId}/reprocess`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.message).toContain('reprocessing');
|
|
expect(response.body.data.job_id).toBe('mock-job-id');
|
|
});
|
|
|
|
it('should return 404 for non-existent receipt', async () => {
|
|
const response = await request
|
|
.post('/api/receipts/999999/reprocess')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
});
|
|
|
|
describe('Receipt Items Management', () => {
|
|
let receiptWithItemsId: number;
|
|
let testItemId: number;
|
|
|
|
beforeAll(async () => {
|
|
const pool = getPool();
|
|
const receiptResult = await pool.query(
|
|
`INSERT INTO public.receipts (user_id, receipt_image_url, status)
|
|
VALUES ($1, '/uploads/receipts/items-test.jpg', 'completed')
|
|
RETURNING receipt_id`,
|
|
[testUser.user.user_id],
|
|
);
|
|
receiptWithItemsId = receiptResult.rows[0].receipt_id;
|
|
createdReceiptIds.push(receiptWithItemsId);
|
|
|
|
const itemResult = await pool.query(
|
|
`INSERT INTO public.receipt_items (receipt_id, raw_item_description, quantity, price_paid_cents, status)
|
|
VALUES ($1, 'EGGS LARGE 12CT', 1, 499, 'unmatched')
|
|
RETURNING receipt_item_id`,
|
|
[receiptWithItemsId],
|
|
);
|
|
testItemId = itemResult.rows[0].receipt_item_id;
|
|
});
|
|
|
|
describe('GET /api/receipts/:receiptId/items', () => {
|
|
it('should return all receipt items', async () => {
|
|
const response = await request
|
|
.get(`/api/receipts/${receiptWithItemsId}/items`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.items).toBeDefined();
|
|
expect(response.body.data.items.length).toBeGreaterThanOrEqual(1);
|
|
expect(response.body.data.total).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
describe('PUT /api/receipts/:receiptId/items/:itemId', () => {
|
|
it('should update item status', async () => {
|
|
const response = await request
|
|
.put(`/api/receipts/${receiptWithItemsId}/items/${testItemId}`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ status: 'matched', match_confidence: 0.95 });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.status).toBe('matched');
|
|
});
|
|
|
|
it('should reject invalid status', async () => {
|
|
const response = await request
|
|
.put(`/api/receipts/${receiptWithItemsId}/items/${testItemId}`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({ status: 'invalid_status' });
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/receipts/:receiptId/items/unadded', () => {
|
|
it('should return unadded items', async () => {
|
|
const response = await request
|
|
.get(`/api/receipts/${receiptWithItemsId}/items/unadded`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.items).toBeDefined();
|
|
expect(Array.isArray(response.body.data.items)).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('POST /api/receipts/:receiptId/confirm - Confirm Items to Inventory', () => {
|
|
let receiptForConfirmId: number;
|
|
let itemToConfirmId: number;
|
|
|
|
beforeAll(async () => {
|
|
const pool = getPool();
|
|
const receiptResult = await pool.query(
|
|
`INSERT INTO public.receipts (user_id, receipt_image_url, status)
|
|
VALUES ($1, '/uploads/receipts/confirm-test.jpg', 'completed')
|
|
RETURNING receipt_id`,
|
|
[testUser.user.user_id],
|
|
);
|
|
receiptForConfirmId = receiptResult.rows[0].receipt_id;
|
|
createdReceiptIds.push(receiptForConfirmId);
|
|
|
|
const itemResult = await pool.query(
|
|
`INSERT INTO public.receipt_items (receipt_id, raw_item_description, quantity, price_paid_cents, status, added_to_pantry)
|
|
VALUES ($1, 'YOGURT GREEK', 2, 798, 'matched', false)
|
|
RETURNING receipt_item_id`,
|
|
[receiptForConfirmId],
|
|
);
|
|
itemToConfirmId = itemResult.rows[0].receipt_item_id;
|
|
});
|
|
|
|
it('should confirm items and add to inventory', async () => {
|
|
const response = await request
|
|
.post(`/api/receipts/${receiptForConfirmId}/confirm`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
items: [
|
|
{
|
|
receipt_item_id: itemToConfirmId,
|
|
include: true,
|
|
item_name: 'Greek Yogurt',
|
|
quantity: 2,
|
|
location: 'fridge',
|
|
expiry_date: '2024-02-15',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.added_items).toBeDefined();
|
|
expect(response.body.data.count).toBeGreaterThanOrEqual(0);
|
|
|
|
// Track created inventory items for cleanup
|
|
if (response.body.data.added_items) {
|
|
response.body.data.added_items.forEach((item: { inventory_id: number }) => {
|
|
if (item.inventory_id) {
|
|
createdInventoryIds.push(item.inventory_id);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
it('should skip items with include: false', async () => {
|
|
const pool = getPool();
|
|
const itemResult = await pool.query(
|
|
`INSERT INTO public.receipt_items (receipt_id, raw_item_description, quantity, price_paid_cents, status, added_to_pantry)
|
|
VALUES ($1, 'CHIPS BBQ', 1, 499, 'matched', false)
|
|
RETURNING receipt_item_id`,
|
|
[receiptForConfirmId],
|
|
);
|
|
const skipItemId = itemResult.rows[0].receipt_item_id;
|
|
|
|
const response = await request
|
|
.post(`/api/receipts/${receiptForConfirmId}/confirm`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
items: [
|
|
{
|
|
receipt_item_id: skipItemId,
|
|
include: false,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
// No items should be added when all are excluded
|
|
});
|
|
|
|
it('should reject invalid location', async () => {
|
|
const response = await request
|
|
.post(`/api/receipts/${receiptForConfirmId}/confirm`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
items: [
|
|
{
|
|
receipt_item_id: itemToConfirmId,
|
|
include: true,
|
|
location: 'invalid_location',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/receipts/:receiptId/logs - Processing Logs', () => {
|
|
let receiptWithLogsId: number;
|
|
|
|
beforeAll(async () => {
|
|
const pool = getPool();
|
|
const receiptResult = await pool.query(
|
|
`INSERT INTO public.receipts (user_id, receipt_image_url, status)
|
|
VALUES ($1, '/uploads/receipts/logs-test.jpg', 'completed')
|
|
RETURNING receipt_id`,
|
|
[testUser.user.user_id],
|
|
);
|
|
receiptWithLogsId = receiptResult.rows[0].receipt_id;
|
|
createdReceiptIds.push(receiptWithLogsId);
|
|
|
|
// Add processing logs - using correct table name and column names
|
|
// processing_step must be one of: upload, ocr_extraction, text_parsing, store_detection,
|
|
// item_extraction, item_matching, price_parsing, finalization
|
|
await pool.query(
|
|
`INSERT INTO public.receipt_processing_log (receipt_id, processing_step, status, error_message)
|
|
VALUES ($1, 'ocr_extraction', 'completed', 'OCR completed successfully'),
|
|
($1, 'item_extraction', 'completed', 'Extracted 5 items'),
|
|
($1, 'item_matching', 'completed', 'Matched 3 items')`,
|
|
[receiptWithLogsId],
|
|
);
|
|
});
|
|
|
|
it('should return processing logs', async () => {
|
|
const response = await request
|
|
.get(`/api/receipts/${receiptWithLogsId}/logs`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.logs).toBeDefined();
|
|
expect(response.body.data.logs.length).toBe(3);
|
|
expect(response.body.data.total).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe('Complete Receipt Workflow', () => {
|
|
it('should handle full upload-process-confirm workflow', async () => {
|
|
// Note: Full workflow with actual processing would require BullMQ worker
|
|
// This test verifies the API contract works correctly
|
|
|
|
// Step 1: Upload receipt
|
|
const testImageBuffer = Buffer.from(
|
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
'base64',
|
|
);
|
|
|
|
const uploadResponse = await request
|
|
.post('/api/receipts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.attach('receipt', testImageBuffer, 'workflow-test.png')
|
|
.field('transaction_date', '2024-01-20');
|
|
|
|
expect(uploadResponse.status).toBe(201);
|
|
const receiptId = uploadResponse.body.data.receipt_id;
|
|
createdReceiptIds.push(receiptId);
|
|
|
|
// Step 2: Verify receipt was created
|
|
const getResponse = await request
|
|
.get(`/api/receipts/${receiptId}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(getResponse.status).toBe(200);
|
|
expect(getResponse.body.data.receipt.receipt_id).toBe(receiptId);
|
|
|
|
// Step 3: Check it appears in list
|
|
const listResponse = await request
|
|
.get('/api/receipts')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(listResponse.status).toBe(200);
|
|
const found = listResponse.body.data.receipts.find(
|
|
(r: { receipt_id: number }) => r.receipt_id === receiptId,
|
|
);
|
|
expect(found).toBeDefined();
|
|
|
|
// Step 4: Verify logs endpoint works (empty for new receipt)
|
|
const logsResponse = await request
|
|
.get(`/api/receipts/${receiptId}/logs`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(logsResponse.status).toBe(200);
|
|
expect(Array.isArray(logsResponse.body.data.logs)).toBe(true);
|
|
});
|
|
});
|
|
});
|