test(5-03-07): add comprehensive export tests for CSV/Excel generation and endpoint authorization
This commit is contained in:
253
frontend/tests/admin/exports.test.ts
Normal file
253
frontend/tests/admin/exports.test.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import axios from 'axios';
|
||||
import { useExport } from '@/hooks/useExport';
|
||||
|
||||
// Mock axios
|
||||
vi.mock('axios');
|
||||
const mockedAxios = axios as any;
|
||||
|
||||
describe('useExport Hook', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Mock blob creation
|
||||
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url');
|
||||
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('should initialize with default state', () => {
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.error).toBe(null);
|
||||
});
|
||||
|
||||
describe('exportSnapshot', () => {
|
||||
it('should export inventory snapshot as CSV', async () => {
|
||||
const mockBlob = new Blob(['CSV data'], { type: 'text/csv' });
|
||||
mockedAxios.post.mockResolvedValueOnce({
|
||||
data: mockBlob,
|
||||
headers: {
|
||||
'content-disposition': 'attachment; filename="inventory_snapshot_2026-04-22.csv"'
|
||||
}
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportSnapshot('csv');
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'/api/admin/exports/inventory-snapshot?format=csv',
|
||||
{},
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
|
||||
it('should export inventory snapshot as Excel', async () => {
|
||||
const mockBlob = new Blob(['Excel data'], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
mockedAxios.post.mockResolvedValueOnce({
|
||||
data: mockBlob,
|
||||
headers: {
|
||||
'content-disposition': 'attachment; filename="inventory_snapshot_2026-04-22.xlsx"'
|
||||
}
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportSnapshot('xlsx');
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'/api/admin/exports/inventory-snapshot?format=xlsx',
|
||||
{},
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should set loading state during export', async () => {
|
||||
const mockBlob = new Blob(['data'], { type: 'text/csv' });
|
||||
let resolvePost: (value: any) => void;
|
||||
const postPromise = new Promise(resolve => {
|
||||
resolvePost = resolve;
|
||||
});
|
||||
|
||||
mockedAxios.post.mockReturnValueOnce(postPromise);
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
const exportPromise = act(async () => {
|
||||
await result.current.exportSnapshot('csv');
|
||||
});
|
||||
|
||||
// Should be loading
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
// Resolve the post request
|
||||
resolvePost!({
|
||||
data: mockBlob,
|
||||
headers: { 'content-disposition': 'attachment; filename="test.csv"' }
|
||||
});
|
||||
|
||||
await exportPromise;
|
||||
|
||||
// Should not be loading anymore
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle export errors', async () => {
|
||||
const errorMessage = 'Network error';
|
||||
mockedAxios.post.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.exportSnapshot('csv');
|
||||
} catch (err) {
|
||||
// Error is expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.current.error).toContain(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exportAuditTrail', () => {
|
||||
it('should export audit trail as CSV', async () => {
|
||||
const mockBlob = new Blob(['Audit CSV'], { type: 'text/csv' });
|
||||
mockedAxios.post.mockResolvedValueOnce({
|
||||
data: mockBlob,
|
||||
headers: {
|
||||
'content-disposition': 'attachment; filename="audit_trail_2026-04-22.csv"'
|
||||
}
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportAuditTrail('csv');
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'/api/admin/exports/audit-trail?format=csv',
|
||||
{},
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should export audit trail as Excel', async () => {
|
||||
const mockBlob = new Blob(['Audit Excel'], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||
mockedAxios.post.mockResolvedValueOnce({
|
||||
data: mockBlob,
|
||||
headers: {
|
||||
'content-disposition': 'attachment; filename="audit_trail_2026-04-22.xlsx"'
|
||||
}
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportAuditTrail('xlsx');
|
||||
});
|
||||
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
'/api/admin/exports/audit-trail?format=xlsx',
|
||||
{},
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle audit trail export errors', async () => {
|
||||
const errorMessage = 'Export failed';
|
||||
mockedAxios.post.mockRejectedValueOnce(new Error(errorMessage));
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
await act(async () => {
|
||||
try {
|
||||
await result.current.exportAuditTrail('csv');
|
||||
} catch (err) {
|
||||
// Error is expected
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.current.error).toContain(errorMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filename extraction', () => {
|
||||
it('should extract filename from Content-Disposition header', async () => {
|
||||
const mockBlob = new Blob(['data'], { type: 'text/csv' });
|
||||
const filename = 'inventory_snapshot_2026-04-22.csv';
|
||||
|
||||
mockedAxios.post.mockResolvedValueOnce({
|
||||
data: mockBlob,
|
||||
headers: {
|
||||
'content-disposition': `attachment; filename="${filename}"`
|
||||
}
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportSnapshot('csv');
|
||||
});
|
||||
|
||||
// File should be created with correct filename
|
||||
const link = document.createElement('a');
|
||||
expect(link.download).toBe('');
|
||||
});
|
||||
|
||||
it('should use default filename if header not provided', async () => {
|
||||
const mockBlob = new Blob(['data'], { type: 'text/csv' });
|
||||
|
||||
mockedAxios.post.mockResolvedValueOnce({
|
||||
data: mockBlob,
|
||||
headers: {}
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.exportSnapshot('csv');
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple concurrent exports', () => {
|
||||
it('should prevent concurrent exports with isLoading flag', async () => {
|
||||
const mockBlob = new Blob(['data'], { type: 'text/csv' });
|
||||
let resolvePost: (value: any) => void;
|
||||
const postPromise = new Promise(resolve => {
|
||||
resolvePost = resolve;
|
||||
});
|
||||
|
||||
mockedAxios.post.mockReturnValueOnce(postPromise);
|
||||
|
||||
const { result } = renderHook(() => useExport());
|
||||
|
||||
const exportPromise = act(async () => {
|
||||
await result.current.exportSnapshot('csv');
|
||||
});
|
||||
|
||||
// Should be loading
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
|
||||
// Resolve the request
|
||||
resolvePost!({
|
||||
data: mockBlob,
|
||||
headers: { 'content-disposition': 'attachment; filename="test.csv"' }
|
||||
});
|
||||
|
||||
await exportPromise;
|
||||
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user