fix(server): at least the watch folder doesn't blow up now
This commit is contained in:
parent
c7a0c25c23
commit
afd01e6075
|
|
@ -1,22 +1,7 @@
|
|||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { screen, fireEvent, waitFor, vi } from '@testing-library/react'
|
||||
import { renderWithMockAuth } from '../../test/test-utils'
|
||||
import Login from '../Login'
|
||||
|
||||
// Mock the auth context
|
||||
const mockLogin = vi.fn()
|
||||
|
||||
vi.mock('../../contexts/AuthContext', () => ({
|
||||
useAuth: () => ({
|
||||
login: mockLogin,
|
||||
user: null,
|
||||
loading: false,
|
||||
register: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
}),
|
||||
AuthProvider: ({ children }: any) => <>{children}</>,
|
||||
}))
|
||||
|
||||
// Mock the API service
|
||||
vi.mock('../../services/api', () => ({
|
||||
api: {
|
||||
|
|
@ -26,11 +11,7 @@ vi.mock('../../services/api', () => ({
|
|||
},
|
||||
}))
|
||||
|
||||
const LoginWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<BrowserRouter>
|
||||
{children}
|
||||
</BrowserRouter>
|
||||
)
|
||||
const mockLogin = vi.fn()
|
||||
|
||||
describe('Login', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -38,11 +19,7 @@ describe('Login', () => {
|
|||
})
|
||||
|
||||
test('renders login form', () => {
|
||||
render(
|
||||
<LoginWrapper>
|
||||
<Login />
|
||||
</LoginWrapper>
|
||||
)
|
||||
renderWithMockAuth(<Login />, { login: mockLogin })
|
||||
|
||||
expect(screen.getByText('Sign in to Readur')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('Username')).toBeInTheDocument()
|
||||
|
|
@ -54,11 +31,7 @@ describe('Login', () => {
|
|||
test('handles form submission with valid credentials', async () => {
|
||||
mockLogin.mockResolvedValue(undefined)
|
||||
|
||||
render(
|
||||
<LoginWrapper>
|
||||
<Login />
|
||||
</LoginWrapper>
|
||||
)
|
||||
renderWithMockAuth(<Login />, { login: mockLogin })
|
||||
|
||||
const usernameInput = screen.getByPlaceholderText('Username')
|
||||
const passwordInput = screen.getByPlaceholderText('Password')
|
||||
|
|
@ -79,11 +52,7 @@ describe('Login', () => {
|
|||
response: { data: { message: errorMessage } },
|
||||
})
|
||||
|
||||
render(
|
||||
<LoginWrapper>
|
||||
<Login />
|
||||
</LoginWrapper>
|
||||
)
|
||||
renderWithMockAuth(<Login />, { login: mockLogin })
|
||||
|
||||
const usernameInput = screen.getByPlaceholderText('Username')
|
||||
const passwordInput = screen.getByPlaceholderText('Password')
|
||||
|
|
@ -101,11 +70,7 @@ describe('Login', () => {
|
|||
test('shows loading state during submission', async () => {
|
||||
mockLogin.mockImplementation(() => new Promise(() => {})) // Never resolves
|
||||
|
||||
render(
|
||||
<LoginWrapper>
|
||||
<Login />
|
||||
</LoginWrapper>
|
||||
)
|
||||
renderWithMockAuth(<Login />, { login: mockLogin })
|
||||
|
||||
const usernameInput = screen.getByPlaceholderText('Username')
|
||||
const passwordInput = screen.getByPlaceholderText('Password')
|
||||
|
|
@ -122,11 +87,7 @@ describe('Login', () => {
|
|||
})
|
||||
|
||||
test('requires username and password', () => {
|
||||
render(
|
||||
<LoginWrapper>
|
||||
<Login />
|
||||
</LoginWrapper>
|
||||
)
|
||||
renderWithMockAuth(<Login />, { login: mockLogin })
|
||||
|
||||
const usernameInput = screen.getByPlaceholderText('Username')
|
||||
const passwordInput = screen.getByPlaceholderText('Password')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react'
|
||||
import { render, RenderOptions } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
}
|
||||
|
||||
interface MockAuthContextType {
|
||||
user: User | null
|
||||
loading: boolean
|
||||
login: (username: string, password: string) => Promise<void>
|
||||
register: (username: string, email: string, password: string) => Promise<void>
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
// Create a mock AuthProvider for testing
|
||||
export const MockAuthProvider = ({
|
||||
children,
|
||||
mockValues = {}
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
mockValues?: Partial<MockAuthContextType>
|
||||
}) => {
|
||||
const defaultMocks = {
|
||||
user: null,
|
||||
loading: false,
|
||||
login: vi.fn(),
|
||||
register: vi.fn(),
|
||||
logout: vi.fn(),
|
||||
...mockValues
|
||||
}
|
||||
|
||||
// Mock the useAuth hook
|
||||
const AuthContext = React.createContext(defaultMocks)
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={defaultMocks}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Create a custom render function that includes providers
|
||||
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<MockAuthProvider>
|
||||
{children}
|
||||
</MockAuthProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export const renderWithProviders = (
|
||||
ui: React.ReactElement,
|
||||
options?: Omit<RenderOptions, 'wrapper'>
|
||||
) => render(ui, { wrapper: AllTheProviders, ...options })
|
||||
|
||||
export const renderWithMockAuth = (
|
||||
ui: React.ReactElement,
|
||||
mockAuthValues?: Partial<MockAuthContextType>,
|
||||
options?: Omit<RenderOptions, 'wrapper'>
|
||||
) => {
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<BrowserRouter>
|
||||
<MockAuthProvider mockValues={mockAuthValues}>
|
||||
{children}
|
||||
</MockAuthProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
return render(ui, { wrapper: Wrapper, ...options })
|
||||
}
|
||||
|
||||
// re-export everything
|
||||
export * from '@testing-library/react'
|
||||
10
src/db.rs
10
src/db.rs
|
|
@ -125,6 +125,14 @@ impl Database {
|
|||
ocr_detect_orientation BOOLEAN DEFAULT TRUE,
|
||||
ocr_whitelist_chars TEXT,
|
||||
ocr_blacklist_chars TEXT,
|
||||
webdav_enabled BOOLEAN DEFAULT FALSE,
|
||||
webdav_server_url TEXT,
|
||||
webdav_username TEXT,
|
||||
webdav_password TEXT,
|
||||
webdav_watch_folders TEXT[] DEFAULT ARRAY['/Documents']::TEXT[],
|
||||
webdav_file_extensions TEXT[] DEFAULT ARRAY['pdf', 'png', 'jpg', 'jpeg', 'tiff', 'bmp', 'txt']::TEXT[],
|
||||
webdav_auto_sync BOOLEAN DEFAULT FALSE,
|
||||
webdav_sync_interval_minutes INTEGER DEFAULT 60,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
)
|
||||
|
|
@ -939,6 +947,8 @@ impl Database {
|
|||
cpu_priority, enable_background_ocr, ocr_page_segmentation_mode, ocr_engine_mode,
|
||||
ocr_min_confidence, ocr_dpi, ocr_enhance_contrast, ocr_remove_noise,
|
||||
ocr_detect_orientation, ocr_whitelist_chars, ocr_blacklist_chars,
|
||||
webdav_enabled, webdav_server_url, webdav_username, webdav_password,
|
||||
webdav_watch_folders, webdav_file_extensions, webdav_auto_sync, webdav_sync_interval_minutes,
|
||||
created_at, updated_at
|
||||
FROM settings WHERE user_id = $1"#
|
||||
)
|
||||
|
|
|
|||
|
|
@ -504,16 +504,21 @@ impl EnhancedOcrService {
|
|||
|
||||
let bytes = std::fs::read(file_path)?;
|
||||
|
||||
// Validate PDF header
|
||||
if bytes.len() < 5 || !bytes.starts_with(b"%PDF-") {
|
||||
// Check if it's a valid PDF (handles leading null bytes)
|
||||
if !is_valid_pdf(&bytes) {
|
||||
return Err(anyhow!(
|
||||
"Invalid PDF file: Missing or corrupted PDF header. File size: {} bytes, Header: {:?}",
|
||||
bytes.len(),
|
||||
bytes.get(0..20).unwrap_or(&[]).iter().map(|&b| b as char).collect::<String>()
|
||||
bytes.get(0..50).unwrap_or(&[]).iter().map(|&b| {
|
||||
if b >= 32 && b <= 126 { b as char } else { '.' }
|
||||
}).collect::<String>()
|
||||
));
|
||||
}
|
||||
|
||||
let text = match pdf_extract::extract_text_from_mem(&bytes) {
|
||||
// Clean the PDF data (remove leading null bytes)
|
||||
let clean_bytes = clean_pdf_data(&bytes);
|
||||
|
||||
let text = match pdf_extract::extract_text_from_mem(&clean_bytes) {
|
||||
Ok(text) => text,
|
||||
Err(e) => {
|
||||
// Provide more detailed error information
|
||||
|
|
@ -631,4 +636,45 @@ impl EnhancedOcrService {
|
|||
pub fn validate_ocr_quality(&self, _result: &OcrResult, _settings: &Settings) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the given bytes represent a valid PDF file
|
||||
/// Handles PDFs with leading null bytes or whitespace
|
||||
fn is_valid_pdf(data: &[u8]) -> bool {
|
||||
if data.len() < 5 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the first occurrence of "%PDF-" in the first 1KB of the file
|
||||
// Some PDFs have leading null bytes or other metadata
|
||||
let search_limit = data.len().min(1024);
|
||||
let search_data = &data[0..search_limit];
|
||||
|
||||
for i in 0..=search_limit.saturating_sub(5) {
|
||||
if &search_data[i..i+5] == b"%PDF-" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Remove leading null bytes and return clean PDF data
|
||||
/// Returns the original data if no PDF header is found
|
||||
fn clean_pdf_data(data: &[u8]) -> Vec<u8> {
|
||||
if data.len() < 5 {
|
||||
return data.to_vec();
|
||||
}
|
||||
|
||||
// Find the first occurrence of "%PDF-" in the first 1KB
|
||||
let search_limit = data.len().min(1024);
|
||||
|
||||
for i in 0..=search_limit.saturating_sub(5) {
|
||||
if &data[i..i+5] == b"%PDF-" {
|
||||
return data[i..].to_vec();
|
||||
}
|
||||
}
|
||||
|
||||
// If no PDF header found, return original data
|
||||
data.to_vec()
|
||||
}
|
||||
|
|
@ -297,12 +297,14 @@ async fn process_file(
|
|||
|
||||
// Validate PDF files before processing
|
||||
if mime_type == "application/pdf" {
|
||||
if file_data.len() < 5 || !file_data.starts_with(b"%PDF-") {
|
||||
if !is_valid_pdf(&file_data) {
|
||||
warn!(
|
||||
"Skipping invalid PDF file: {} (size: {} bytes, header: {:?})",
|
||||
filename,
|
||||
file_data.len(),
|
||||
file_data.get(0..20).unwrap_or(&[]).iter().map(|&b| b as char).collect::<String>()
|
||||
file_data.get(0..50).unwrap_or(&[]).iter().map(|&b| {
|
||||
if b >= 32 && b <= 126 { b as char } else { '.' }
|
||||
}).collect::<String>()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -369,4 +371,45 @@ fn calculate_priority(file_size: i64, mime_type: &str) -> i32 {
|
|||
};
|
||||
|
||||
(base_priority + type_boost).min(10)
|
||||
}
|
||||
|
||||
/// Check if the given bytes represent a valid PDF file
|
||||
/// Handles PDFs with leading null bytes or whitespace
|
||||
fn is_valid_pdf(data: &[u8]) -> bool {
|
||||
if data.len() < 5 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the first occurrence of "%PDF-" in the first 1KB of the file
|
||||
// Some PDFs have leading null bytes or other metadata
|
||||
let search_limit = data.len().min(1024);
|
||||
let search_data = &data[0..search_limit];
|
||||
|
||||
for i in 0..=search_limit.saturating_sub(5) {
|
||||
if &search_data[i..i+5] == b"%PDF-" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Remove leading null bytes and return clean PDF data
|
||||
/// Returns the original data if no PDF header is found
|
||||
fn clean_pdf_data(data: &[u8]) -> &[u8] {
|
||||
if data.len() < 5 {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Find the first occurrence of "%PDF-" in the first 1KB
|
||||
let search_limit = data.len().min(1024);
|
||||
|
||||
for i in 0..=search_limit.saturating_sub(5) {
|
||||
if &data[i..i+5] == b"%PDF-" {
|
||||
return &data[i..];
|
||||
}
|
||||
}
|
||||
|
||||
// If no PDF header found, return original data
|
||||
data
|
||||
}
|
||||
Loading…
Reference in New Issue