mirror of
https://github.com/run-llama/auto_rfp.git
synced 2026-07-01 21:54:05 -04:00
1151372e5c
* Add agentic mode support for LlamaParse and fix eligibility extraction
- Add agenticMode option to ParseOptions interface and Zod schema
- Enable agentic parsing by default for better multi-sheet Excel support
- Fix eligibility extraction to gracefully handle documents without
eligibility criteria by returning empty array instead of throwing error
* Refactor project index selection to single-select with auto-save
- Change ProjectIndexSelector from multi-select checkboxes to single-select
clickable rows with radio-style indicators
- Implement debounced auto-save (800ms) when selection changes
- Remove Edit/Save/Cancel buttons in favor of automatic persistence
- Add refreshKey prop to ProjectDocuments for efficient refetch on changes
- Update DocumentsSection to wire components with onSaveSuccess callback
- Simplify Questions page index selector to show index name with Active badge
* Fix ESLint configuration and resolve all lint errors
- Add eslint and eslint-config-next as dev dependencies
- Create .eslintrc.json with next/core-web-vitals preset
- Fix 43 unescaped entity errors by replacing quotes and apostrophes
with HTML entities across 14 component files
- Fix 9 react-hooks/exhaustive-deps warnings by wrapping fetch
functions in useCallback with proper dependency arrays
* Add Vitest testing infrastructure with 173 unit tests
- Configure Vitest with coverage reporting and path aliases
- Add test scripts (test, test:run, test:coverage) to package.json
- Install vitest, @vitest/coverage-v8, vite-tsconfig-paths, vitest-mock-extended
Test coverage includes:
- validators: extract-questions, generate-response, llamaparse, multi-step-response
- errors: all API error classes with type guard
- services: FileValidator (file type/size validation), DefaultResponseService
- middleware: apiHandler and withApiHandler request validation
Add mock infrastructure for Prisma, OpenAI, and test fixtures
* Fix missing agenticMode in LlamaParse processing service
Add agentic_mode field to request schema and form data parser to ensure
the agenticMode option is properly passed through the parsing pipeline.
This resolves TypeScript compilation error where agenticMode was required
in LlamaParseOptions but not provided by the processing service.
* Fix Add Manually button routing and improve Vercel preview URL handling
- Fix 404 error when clicking Add Manually button by using correct route path
/projects/{projectId}/questions/create instead of /questions/create?projectId=
- Update magic link auth to use VERCEL_URL for preview deployments
* Simplify IndexSelector to display-only mode
Remove index configuration UI since only single file selection is now supported:
- Remove Configure/Hide toggle button and collapsible panel
- Remove Select All/Deselect All functionality
- Remove checkbox selection grid for indexes
- Clean up handleIndexToggle and handleSelectAllIndexes from provider
- Keep project card display showing active index name and status
299 lines
9.0 KiB
TypeScript
299 lines
9.0 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { z } from 'zod';
|
|
import { NextResponse } from 'next/server';
|
|
import { apiHandler, withApiHandler } from '@/lib/middleware/api-handler';
|
|
import {
|
|
ValidationError,
|
|
AuthorizationError,
|
|
ForbiddenError,
|
|
NotFoundError,
|
|
ApiError,
|
|
} from '@/lib/errors/api-errors';
|
|
|
|
// Mock NextRequest
|
|
const createMockRequest = (body: any = {}) => ({
|
|
json: vi.fn().mockResolvedValue(body),
|
|
method: 'POST',
|
|
headers: new Headers(),
|
|
url: 'http://localhost:3000/api/test',
|
|
});
|
|
|
|
// Helper to create NextResponse for withApiHandler tests
|
|
const createJsonResponse = (data: any, status = 200) =>
|
|
NextResponse.json(data, { status });
|
|
|
|
describe('apiHandler', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should return success response for successful handler', async () => {
|
|
const handler = async () => ({ data: 'test', success: true });
|
|
const response = await apiHandler(handler);
|
|
|
|
expect(response.status).toBe(200);
|
|
const body = await response.json();
|
|
expect(body.data).toBe('test');
|
|
expect(body.success).toBe(true);
|
|
});
|
|
|
|
it('should return JSON response with data', async () => {
|
|
const testData = { items: [1, 2, 3], count: 3 };
|
|
const handler = async () => testData;
|
|
const response = await apiHandler(handler);
|
|
|
|
const body = await response.json();
|
|
expect(body).toEqual(testData);
|
|
});
|
|
|
|
it('should return 400 for ValidationError', async () => {
|
|
const handler = async () => {
|
|
throw new ValidationError('Invalid input');
|
|
};
|
|
|
|
const response = await apiHandler(handler);
|
|
expect(response.status).toBe(400);
|
|
|
|
const body = await response.json();
|
|
expect(body.error).toBe('Invalid input');
|
|
expect(body.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
|
|
it('should include details for ValidationError', async () => {
|
|
const details = [{ path: ['email'], message: 'Invalid email format' }];
|
|
const handler = async () => {
|
|
throw new ValidationError('Validation failed', details);
|
|
};
|
|
|
|
const response = await apiHandler(handler);
|
|
const body = await response.json();
|
|
expect(body.details).toEqual(details);
|
|
});
|
|
|
|
it('should return 401 for AuthorizationError', async () => {
|
|
const handler = async () => {
|
|
throw new AuthorizationError('Token expired');
|
|
};
|
|
|
|
const response = await apiHandler(handler);
|
|
expect(response.status).toBe(401);
|
|
|
|
const body = await response.json();
|
|
expect(body.error).toBe('Token expired');
|
|
expect(body.code).toBe('UNAUTHORIZED');
|
|
});
|
|
|
|
it('should return 403 for ForbiddenError', async () => {
|
|
const handler = async () => {
|
|
throw new ForbiddenError('Admin access required');
|
|
};
|
|
|
|
const response = await apiHandler(handler);
|
|
expect(response.status).toBe(403);
|
|
|
|
const body = await response.json();
|
|
expect(body.error).toBe('Admin access required');
|
|
expect(body.code).toBe('FORBIDDEN');
|
|
});
|
|
|
|
it('should return 404 for NotFoundError', async () => {
|
|
const handler = async () => {
|
|
throw new NotFoundError('Project not found');
|
|
};
|
|
|
|
const response = await apiHandler(handler);
|
|
expect(response.status).toBe(404);
|
|
|
|
const body = await response.json();
|
|
expect(body.error).toBe('Project not found');
|
|
expect(body.code).toBe('NOT_FOUND');
|
|
});
|
|
|
|
it('should return custom status code for ApiError', async () => {
|
|
const handler = async () => {
|
|
throw new ApiError('Custom error', 418, 'TEAPOT');
|
|
};
|
|
|
|
const response = await apiHandler(handler);
|
|
expect(response.status).toBe(418);
|
|
|
|
const body = await response.json();
|
|
expect(body.error).toBe('Custom error');
|
|
expect(body.code).toBe('TEAPOT');
|
|
});
|
|
|
|
it('should return 500 for unexpected errors', async () => {
|
|
const handler = async () => {
|
|
throw new Error('Unexpected error');
|
|
};
|
|
|
|
const response = await apiHandler(handler);
|
|
expect(response.status).toBe(500);
|
|
|
|
const body = await response.json();
|
|
expect(body.error).toBe('Internal server error');
|
|
});
|
|
|
|
it('should not leak error details for unexpected errors', async () => {
|
|
const handler = async () => {
|
|
throw new Error('Database password exposed');
|
|
};
|
|
|
|
const response = await apiHandler(handler);
|
|
const body = await response.json();
|
|
expect(body.error).not.toContain('password');
|
|
expect(body.error).toBe('Internal server error');
|
|
});
|
|
});
|
|
|
|
describe('withApiHandler', () => {
|
|
const TestSchema = z.object({
|
|
name: z.string().min(1, 'Name is required'),
|
|
value: z.number().positive('Value must be positive'),
|
|
});
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
it('should validate request body with schema', async () => {
|
|
const handler = vi.fn().mockImplementation(() =>
|
|
createJsonResponse({ success: true })
|
|
);
|
|
const wrappedHandler = withApiHandler(handler, {
|
|
validationSchema: TestSchema,
|
|
});
|
|
|
|
const mockRequest = createMockRequest({ name: 'test', value: 42 });
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(handler).toHaveBeenCalledWith(mockRequest, { name: 'test', value: 42 });
|
|
});
|
|
|
|
it('should reject invalid request body', async () => {
|
|
const handler = vi.fn();
|
|
const wrappedHandler = withApiHandler(handler, {
|
|
validationSchema: TestSchema,
|
|
});
|
|
|
|
const mockRequest = createMockRequest({ name: '', value: -5 });
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(handler).not.toHaveBeenCalled();
|
|
|
|
const body = await response.json();
|
|
expect(body.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
|
|
it('should include validation error details', async () => {
|
|
const handler = vi.fn();
|
|
const wrappedHandler = withApiHandler(handler, {
|
|
validationSchema: TestSchema,
|
|
});
|
|
|
|
const mockRequest = createMockRequest({ name: '', value: 'not a number' });
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
|
|
const body = await response.json();
|
|
expect(body.details).toBeDefined();
|
|
expect(Array.isArray(body.details)).toBe(true);
|
|
});
|
|
|
|
it('should skip validation when skipValidation is true', async () => {
|
|
const handler = vi.fn().mockImplementation(() =>
|
|
createJsonResponse({ skipped: true })
|
|
);
|
|
const wrappedHandler = withApiHandler(handler, {
|
|
skipValidation: true,
|
|
});
|
|
|
|
const mockRequest = createMockRequest({ any: 'data' });
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(handler).toHaveBeenCalledWith(mockRequest, {});
|
|
});
|
|
|
|
it('should parse JSON body without schema', async () => {
|
|
const handler = vi.fn().mockImplementation(() =>
|
|
createJsonResponse({ received: true })
|
|
);
|
|
const wrappedHandler = withApiHandler(handler, {});
|
|
|
|
const testBody = { custom: 'data', nested: { value: 123 } };
|
|
const mockRequest = createMockRequest(testBody);
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(handler).toHaveBeenCalledWith(mockRequest, testBody);
|
|
});
|
|
|
|
it('should handle JSON parse errors', async () => {
|
|
const handler = vi.fn();
|
|
const wrappedHandler = withApiHandler(handler, {});
|
|
|
|
const mockRequest = {
|
|
json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
|
|
};
|
|
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
expect(response.status).toBe(500);
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should propagate handler errors correctly', async () => {
|
|
const handler = vi.fn().mockRejectedValue(new NotFoundError('Not found'));
|
|
const wrappedHandler = withApiHandler(handler, {
|
|
validationSchema: TestSchema,
|
|
});
|
|
|
|
const mockRequest = createMockRequest({ name: 'test', value: 1 });
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
|
|
expect(response.status).toBe(404);
|
|
});
|
|
|
|
it('should handle missing required fields', async () => {
|
|
const handler = vi.fn();
|
|
const wrappedHandler = withApiHandler(handler, {
|
|
validationSchema: TestSchema,
|
|
});
|
|
|
|
const mockRequest = createMockRequest({ name: 'test' }); // missing value
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(handler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should validate complex nested schemas', async () => {
|
|
const NestedSchema = z.object({
|
|
user: z.object({
|
|
name: z.string(),
|
|
email: z.string().email(),
|
|
}),
|
|
items: z.array(z.string()),
|
|
});
|
|
|
|
const handler = vi.fn().mockImplementation(() =>
|
|
createJsonResponse({ ok: true })
|
|
);
|
|
const wrappedHandler = withApiHandler(handler, {
|
|
validationSchema: NestedSchema,
|
|
});
|
|
|
|
const validData = {
|
|
user: { name: 'Test', email: 'test@example.com' },
|
|
items: ['a', 'b'],
|
|
};
|
|
|
|
const mockRequest = createMockRequest(validData);
|
|
const response = await wrappedHandler(mockRequest as any);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(handler).toHaveBeenCalledWith(mockRequest, validData);
|
|
});
|
|
});
|