Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3e233bf38 | ||
| 1696aeb54f |
@@ -1 +1 @@
|
||||
FORCE_COLOR=0 npx lint-staged
|
||||
FORCE_COLOR=0 npx lint-staged --quiet
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.11.16",
|
||||
"version": "0.11.17",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "flyer-crawler",
|
||||
"version": "0.11.16",
|
||||
"version": "0.11.17",
|
||||
"dependencies": {
|
||||
"@bull-board/api": "^6.14.2",
|
||||
"@bull-board/express": "^6.14.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "flyer-crawler",
|
||||
"private": true,
|
||||
"version": "0.11.16",
|
||||
"version": "0.11.17",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"npm:start:dev\" \"vite\"",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// src/features/shopping/WatchedItemsList.test.tsx
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { WatchedItemsList } from './WatchedItemsList';
|
||||
@@ -99,55 +99,6 @@ describe('WatchedItemsList (in shopping feature)', () => {
|
||||
expect(screen.getByText('Bread')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow adding a new item', async () => {
|
||||
renderWithQueryClient(<WatchedItemsList {...defaultProps} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/add item/i), { target: { value: 'Cheese' } });
|
||||
// Use getByDisplayValue to reliably select the category dropdown, which has no label.
|
||||
// Also, use the correct category name from the CATEGORIES constant.
|
||||
const categorySelect = screen.getByDisplayValue('Select a category');
|
||||
fireEvent.change(categorySelect, { target: { value: 'Dairy & Eggs' } });
|
||||
|
||||
fireEvent.submit(screen.getByRole('button', { name: 'Add' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnAddItem).toHaveBeenCalledWith('Cheese', 'Dairy & Eggs');
|
||||
});
|
||||
|
||||
// Check if form resets
|
||||
expect(screen.getByPlaceholderText(/add item/i)).toHaveValue('');
|
||||
});
|
||||
|
||||
it('should show a loading spinner while adding an item', async () => {
|
||||
// Create a promise that we can resolve manually to control the loading state
|
||||
let resolvePromise: (value: void | PromiseLike<void>) => void;
|
||||
const mockPromise = new Promise<void>((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
mockOnAddItem.mockImplementation(() => mockPromise);
|
||||
|
||||
renderWithQueryClient(<WatchedItemsList {...defaultProps} />);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/add item/i), { target: { value: 'Cheese' } });
|
||||
fireEvent.change(screen.getByDisplayValue('Select a category'), {
|
||||
target: { value: 'Dairy & Eggs' },
|
||||
});
|
||||
const addButton = screen.getByRole('button', { name: 'Add' });
|
||||
fireEvent.click(addButton);
|
||||
|
||||
// The button text is replaced by the spinner, so we use the captured reference
|
||||
await waitFor(() => {
|
||||
expect(addButton).toBeDisabled();
|
||||
});
|
||||
expect(addButton.querySelector('.animate-spin')).toBeInTheDocument();
|
||||
|
||||
// Resolve the promise to complete the async operation and allow the test to finish
|
||||
await act(async () => {
|
||||
resolvePromise();
|
||||
await mockPromise;
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow removing an item', async () => {
|
||||
renderWithQueryClient(<WatchedItemsList {...defaultProps} />);
|
||||
const removeButton = screen.getByRole('button', { name: /remove apples/i });
|
||||
@@ -219,55 +170,6 @@ describe('WatchedItemsList (in shopping feature)', () => {
|
||||
expect(screen.getByText(/your watchlist is empty/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Form Validation and Disabled States', () => {
|
||||
it('should disable the "Add" button if item name is empty or whitespace', () => {
|
||||
renderWithQueryClient(<WatchedItemsList {...defaultProps} />);
|
||||
const nameInput = screen.getByPlaceholderText(/add item/i);
|
||||
const categorySelect = screen.getByDisplayValue('Select a category');
|
||||
const addButton = screen.getByRole('button', { name: 'Add' });
|
||||
|
||||
// Initially disabled
|
||||
expect(addButton).toBeDisabled();
|
||||
|
||||
// With category but no name
|
||||
fireEvent.change(categorySelect, { target: { value: 'Fruits & Vegetables' } });
|
||||
expect(addButton).toBeDisabled();
|
||||
|
||||
// With whitespace name
|
||||
fireEvent.change(nameInput, { target: { value: ' ' } });
|
||||
expect(addButton).toBeDisabled();
|
||||
|
||||
// With valid name
|
||||
fireEvent.change(nameInput, { target: { value: 'Grapes' } });
|
||||
expect(addButton).toBeEnabled();
|
||||
});
|
||||
|
||||
it('should disable the "Add" button if category is not selected', () => {
|
||||
renderWithQueryClient(<WatchedItemsList {...defaultProps} />);
|
||||
const nameInput = screen.getByPlaceholderText(/add item/i);
|
||||
const addButton = screen.getByRole('button', { name: 'Add' });
|
||||
|
||||
// Initially disabled
|
||||
expect(addButton).toBeDisabled();
|
||||
|
||||
// With name but no category
|
||||
fireEvent.change(nameInput, { target: { value: 'Grapes' } });
|
||||
expect(addButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should not submit if form is submitted with invalid data', () => {
|
||||
renderWithQueryClient(<WatchedItemsList {...defaultProps} />);
|
||||
const nameInput = screen.getByPlaceholderText(/add item/i);
|
||||
const form = nameInput.closest('form')!;
|
||||
const categorySelect = screen.getByDisplayValue('Select a category');
|
||||
fireEvent.change(categorySelect, { target: { value: 'Dairy & Eggs' } });
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: ' ' } });
|
||||
fireEvent.submit(form);
|
||||
expect(mockOnAddItem).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('UI Edge Cases', () => {
|
||||
it('should display a specific message when a filter results in no items', () => {
|
||||
const { rerender } = render(<WatchedItemsList {...defaultProps} />);
|
||||
|
||||
@@ -60,13 +60,11 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
||||
await pool.query('DELETE FROM public.user_watched_items WHERE user_id = $1', [userId]);
|
||||
}
|
||||
|
||||
// Clean up flyer items (disable trigger to avoid issues with NULL master_item_id)
|
||||
// Clean up flyer items (master_item_id has ON DELETE SET NULL constraint, so no trigger disable needed)
|
||||
if (createdFlyerIds.length > 0) {
|
||||
await pool.query('ALTER TABLE public.flyer_items DISABLE TRIGGER ALL');
|
||||
await pool.query('DELETE FROM public.flyer_items WHERE flyer_id = ANY($1::bigint[])', [
|
||||
createdFlyerIds,
|
||||
]);
|
||||
await pool.query('ALTER TABLE public.flyer_items ENABLE TRIGGER ALL');
|
||||
}
|
||||
|
||||
// Clean up flyers
|
||||
@@ -147,6 +145,20 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
||||
const beveragesData = await beveragesResponse.json();
|
||||
const beveragesCategoryId = beveragesData.data.category_id;
|
||||
|
||||
const produceResponse = await authedFetch(
|
||||
'/categories/lookup?name=' + encodeURIComponent('Fruits & Vegetables'),
|
||||
{ method: 'GET' },
|
||||
);
|
||||
const produceData = await produceResponse.json();
|
||||
const produceCategoryId = produceData.data.category_id;
|
||||
|
||||
const meatResponse = await authedFetch(
|
||||
'/categories/lookup?name=' + encodeURIComponent('Meat & Seafood'),
|
||||
{ method: 'GET' },
|
||||
);
|
||||
const meatData = await meatResponse.json();
|
||||
const meatCategoryId = meatData.data.category_id;
|
||||
|
||||
// NOTE: The watched items API now uses category_id (number) as of Phase 3.
|
||||
// Category names are no longer accepted. Use the category discovery endpoints
|
||||
// to look up category IDs before creating watched items.
|
||||
@@ -199,21 +211,21 @@ describe('E2E Deals and Price Tracking Journey', () => {
|
||||
createdStoreLocations.push(store2);
|
||||
const store2Id = store2.storeId;
|
||||
|
||||
// Create master grocery items
|
||||
// Create master grocery items with categories
|
||||
const items = [
|
||||
'E2E Milk 2%',
|
||||
'E2E Bread White',
|
||||
'E2E Coffee Beans',
|
||||
'E2E Bananas',
|
||||
'E2E Chicken Breast',
|
||||
{ name: 'E2E Milk 2%', category_id: dairyEggsCategoryId },
|
||||
{ name: 'E2E Bread White', category_id: bakeryCategoryId },
|
||||
{ name: 'E2E Coffee Beans', category_id: beveragesCategoryId },
|
||||
{ name: 'E2E Bananas', category_id: produceCategoryId },
|
||||
{ name: 'E2E Chicken Breast', category_id: meatCategoryId },
|
||||
];
|
||||
|
||||
for (const itemName of items) {
|
||||
for (const item of items) {
|
||||
const result = await pool.query(
|
||||
`INSERT INTO public.master_grocery_items (name)
|
||||
VALUES ($1)
|
||||
`INSERT INTO public.master_grocery_items (name, category_id)
|
||||
VALUES ($1, $2)
|
||||
RETURNING master_grocery_item_id`,
|
||||
[itemName],
|
||||
[item.name, item.category_id],
|
||||
);
|
||||
createdMasterItemIds.push(result.rows[0].master_grocery_item_id);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* 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 { describe, it, expect, beforeAll, afterAll, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import path from 'path';
|
||||
import type { UserProfile } from '../../types';
|
||||
import { createAndLoginUser } from '../utils/testHelpers';
|
||||
import { cleanupDb } from '../utils/cleanup';
|
||||
@@ -14,50 +15,76 @@ import {
|
||||
cleanupStoreLocations,
|
||||
type CreatedStoreLocation,
|
||||
} from '../utils/storeHelpers';
|
||||
import { cleanupFiles } from '../utils/cleanupFiles';
|
||||
|
||||
/**
|
||||
* @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(),
|
||||
}));
|
||||
// Storage path for test files
|
||||
const testStoragePath =
|
||||
process.env.STORAGE_PATH || path.resolve(__dirname, '../../../uploads/receipts');
|
||||
|
||||
// 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' }),
|
||||
},
|
||||
}));
|
||||
// Mock storage service to write files to disk AND return URLs (like flyer-processing)
|
||||
vi.mock('../../services/storage/storageService', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const fsModule = require('node:fs/promises');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const pathModule = require('path');
|
||||
|
||||
return {
|
||||
storageService: {
|
||||
upload: vi
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
async (
|
||||
fileData: Buffer | string | { name?: string; path?: string },
|
||||
fileName?: string,
|
||||
) => {
|
||||
const name =
|
||||
fileName ||
|
||||
(fileData && typeof fileData === 'object' && 'name' in fileData && fileData.name) ||
|
||||
(typeof fileData === 'string'
|
||||
? pathModule.basename(fileData)
|
||||
: `upload-${Date.now()}.jpg`);
|
||||
|
||||
// Use the STORAGE_PATH from the environment (set by global setup to temp directory)
|
||||
const uploadDir =
|
||||
process.env.STORAGE_PATH || pathModule.join(process.cwd(), 'uploads', 'receipts');
|
||||
await fsModule.mkdir(uploadDir, { recursive: true });
|
||||
const destPath = pathModule.join(uploadDir, name);
|
||||
|
||||
let content: Buffer = Buffer.from('');
|
||||
if (Buffer.isBuffer(fileData)) {
|
||||
content = Buffer.from(fileData);
|
||||
} else if (typeof fileData === 'string') {
|
||||
try {
|
||||
content = await fsModule.readFile(fileData);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
} else if (
|
||||
fileData &&
|
||||
typeof fileData === 'object' &&
|
||||
'path' in fileData &&
|
||||
fileData.path
|
||||
) {
|
||||
try {
|
||||
content = await fsModule.readFile(fileData.path);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
await fsModule.writeFile(destPath, content);
|
||||
|
||||
// Return a valid URL to satisfy the 'url_check' DB constraint
|
||||
return `https://example.com/uploads/receipts/${name}`;
|
||||
},
|
||||
),
|
||||
delete: vi.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
||||
let request: ReturnType<typeof supertest>;
|
||||
@@ -67,10 +94,18 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
||||
const createdReceiptIds: number[] = [];
|
||||
const createdInventoryIds: number[] = [];
|
||||
const createdStoreLocations: CreatedStoreLocation[] = [];
|
||||
const createdFilePaths: string[] = [];
|
||||
|
||||
const originalFrontendUrl = process.env.FRONTEND_URL;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Stub FRONTEND_URL to ensure valid absolute URLs
|
||||
vi.stubEnv('FRONTEND_URL', 'https://example.com');
|
||||
const app = (await import('../../../server')).default;
|
||||
vi.stubEnv('STORAGE_PATH', testStoragePath);
|
||||
process.env.FRONTEND_URL = 'https://example.com';
|
||||
|
||||
const appModule = await import('../../../server');
|
||||
const app = appModule.default;
|
||||
request = supertest(app);
|
||||
|
||||
// Create a user for receipt tests
|
||||
@@ -84,14 +119,39 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
||||
createdUserIds.push(user.user.user_id);
|
||||
});
|
||||
|
||||
// Reset mocks before each test to ensure isolation
|
||||
beforeEach(async () => {
|
||||
console.error('[TEST SETUP] Resetting mocks before test execution');
|
||||
// Add any mock resets here if needed for receipt processing
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Restore original value
|
||||
process.env.FRONTEND_URL = originalFrontendUrl;
|
||||
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// CRITICAL: Close workers FIRST before any cleanup to ensure no pending jobs
|
||||
try {
|
||||
console.error('[TEST TEARDOWN] Closing in-process workers...');
|
||||
const { closeWorkers } = await import('../../services/workers.server');
|
||||
await closeWorkers();
|
||||
// Give workers a moment to fully release resources
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
console.error('[TEST TEARDOWN] Error closing workers:', error);
|
||||
}
|
||||
|
||||
// Close the shared redis connection used by the workers/queues
|
||||
const { connection } = await import('../../services/redis.server');
|
||||
await connection.quit();
|
||||
|
||||
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[])', [
|
||||
await pool.query('DELETE FROM public.pantry_items WHERE pantry_item_id = ANY($1::int[])', [
|
||||
createdInventoryIds,
|
||||
]);
|
||||
}
|
||||
@@ -112,9 +172,31 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
||||
|
||||
await cleanupDb({ userIds: createdUserIds });
|
||||
await cleanupStoreLocations(pool, createdStoreLocations);
|
||||
|
||||
// Clean up test files
|
||||
await cleanupFiles(createdFilePaths);
|
||||
|
||||
// Final delay to let any remaining async operations settle
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
|
||||
describe('POST /api/receipts - Upload Receipt', () => {
|
||||
let testStoreLocationId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Create a test store for receipt upload tests
|
||||
const pool = getPool();
|
||||
const store = await createStoreWithLocation(pool, {
|
||||
name: `Receipt Upload Test Store - ${Date.now()}`,
|
||||
address: '123 Receipt St',
|
||||
city: 'Toronto',
|
||||
province: 'ON',
|
||||
postalCode: 'M5V 1A1',
|
||||
});
|
||||
createdStoreLocations.push(store);
|
||||
testStoreLocationId = store.storeLocationId;
|
||||
});
|
||||
|
||||
it('should upload a receipt image successfully', async () => {
|
||||
// Create a simple test image buffer
|
||||
const testImageBuffer = Buffer.from(
|
||||
@@ -126,15 +208,18 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
||||
.post('/api/receipts')
|
||||
.set('Authorization', `Bearer ${authToken}`)
|
||||
.attach('receipt', testImageBuffer, 'test-receipt.png')
|
||||
.field('store_location_id', '1')
|
||||
.field('store_location_id', testStoreLocationId.toString())
|
||||
.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');
|
||||
expect(response.body.data.job_id).toBeDefined(); // Real queue job ID
|
||||
|
||||
createdReceiptIds.push(response.body.data.receipt_id);
|
||||
|
||||
// Track the uploaded file for cleanup
|
||||
createdFilePaths.push(path.join(testStoragePath, 'test-receipt.png'));
|
||||
});
|
||||
|
||||
it('should upload receipt without optional fields', async () => {
|
||||
@@ -152,6 +237,9 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
||||
expect(response.body.data.receipt_id).toBeDefined();
|
||||
|
||||
createdReceiptIds.push(response.body.data.receipt_id);
|
||||
|
||||
// Track the uploaded file for cleanup
|
||||
createdFilePaths.push(path.join(testStoragePath, 'test-receipt-2.png'));
|
||||
});
|
||||
|
||||
it('should reject request without file', async () => {
|
||||
@@ -370,7 +458,7 @@ describe('Receipt Processing Integration Tests (/api/receipts)', () => {
|
||||
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');
|
||||
expect(response.body.data.job_id).toBeDefined(); // Real queue job ID
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent receipt', async () => {
|
||||
|
||||
@@ -164,21 +164,38 @@ vi.mock('jsonwebtoken', () => ({
|
||||
// Mock 'bcrypt'. The service uses `import * as bcrypt from 'bcrypt'`.
|
||||
vi.mock('bcrypt');
|
||||
|
||||
// Mock 'crypto'. The service uses `import crypto from 'crypto'`.
|
||||
vi.mock('crypto', () => ({
|
||||
default: {
|
||||
randomBytes: vi.fn().mockReturnValue({
|
||||
toString: vi.fn().mockImplementation((encoding) => {
|
||||
const id = 'mocked_random_id';
|
||||
console.log(
|
||||
`[DEBUG] tests-setup-unit.ts: crypto.randomBytes mock returning "${id}" for encoding "${encoding}"`,
|
||||
);
|
||||
return id;
|
||||
}),
|
||||
}),
|
||||
randomUUID: vi.fn().mockReturnValue('mocked_random_id'),
|
||||
},
|
||||
}));
|
||||
// Mock 'crypto'. Supports both default import and named imports.
|
||||
// Default: import crypto from 'crypto'; crypto.randomUUID()
|
||||
// Named: import { randomUUID } from 'crypto'; randomUUID()
|
||||
vi.mock('crypto', async () => {
|
||||
const actual = await vi.importActual<typeof import('crypto')>('crypto');
|
||||
const mockRandomUUID = vi.fn(() => actual.randomUUID());
|
||||
const mockRandomBytes = vi.fn((size: number) => {
|
||||
const buffer = actual.randomBytes(size);
|
||||
// Add mocked toString for backward compatibility
|
||||
buffer.toString = vi.fn().mockImplementation((encoding) => {
|
||||
const id = 'mocked_random_id';
|
||||
console.log(
|
||||
`[DEBUG] tests-setup-unit.ts: crypto.randomBytes mock returning "${id}" for encoding "${encoding}"`,
|
||||
);
|
||||
return id;
|
||||
});
|
||||
return buffer;
|
||||
});
|
||||
|
||||
return {
|
||||
...actual,
|
||||
// Named exports for: import { randomUUID } from 'crypto'
|
||||
randomUUID: mockRandomUUID,
|
||||
randomBytes: mockRandomBytes,
|
||||
// Default export for: import crypto from 'crypto'
|
||||
default: {
|
||||
...actual,
|
||||
randomUUID: mockRandomUUID,
|
||||
randomBytes: mockRandomBytes,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// --- Global Mocks ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user