diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts
index 1406ba1..f24d144 100644
--- a/frontend/e2e/auth.spec.ts
+++ b/frontend/e2e/auth.spec.ts
@@ -1,12 +1,10 @@
-import { test, expect } from '@playwright/test';
-import { TEST_USERS, TIMEOUTS } from './utils/test-data';
+import { test, expect, AuthHelper, TEST_CREDENTIALS, TIMEOUTS } from './fixtures/auth';
import { TestHelpers } from './utils/test-helpers';
test.describe('Authentication', () => {
- let helpers: TestHelpers;
-
test.beforeEach(async ({ page }) => {
- helpers = new TestHelpers(page);
+ const authHelper = new AuthHelper(page);
+ await authHelper.ensureLoggedOut();
});
test('should display login form on initial visit', async ({ page }) => {
@@ -19,22 +17,12 @@ test.describe('Authentication', () => {
});
test('should login with valid credentials', async ({ page }) => {
- await page.goto('/');
+ const authHelper = new AuthHelper(page);
- // Fill login form with demo credentials
- await page.fill('input[name="username"]', 'admin');
- await page.fill('input[name="password"]', 'readur2024');
-
- // Wait for login API call
- const loginResponse = helpers.waitForApiCall('/auth/login');
-
- await page.click('button[type="submit"]');
-
- // Verify login was successful
- await loginResponse;
+ await authHelper.loginAs(TEST_CREDENTIALS.admin);
// Should redirect to dashboard or main page
- await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium });
+ await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.navigation });
// Verify we're no longer on login page
await expect(page.locator('input[name="username"]')).not.toBeVisible();
@@ -49,20 +37,19 @@ test.describe('Authentication', () => {
await page.click('button[type="submit"]');
// Should show error message (Material-UI Alert)
- await expect(page.locator('.MuiAlert-root, [role="alert"]')).toBeVisible({ timeout: TIMEOUTS.short });
+ await expect(page.locator('.MuiAlert-root, [role="alert"]')).toBeVisible({ timeout: TIMEOUTS.api });
// Should remain on login page
await expect(page.locator('input[name="username"]')).toBeVisible();
});
test.skip('should logout successfully', async ({ page }) => {
- // First login
- await page.goto('/');
- await page.fill('input[name="username"]', 'admin');
- await page.fill('input[name="password"]', 'readur2024');
- await page.click('button[type="submit"]');
+ const authHelper = new AuthHelper(page);
- await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium });
+ // First login
+ await authHelper.loginAs(TEST_CREDENTIALS.admin);
+
+ await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.navigation });
// Find and click profile/account button in the top app bar (has AccountIcon)
const profileButton = page.locator('button:has([data-testid="AccountCircleIcon"])');
@@ -73,18 +60,17 @@ test.describe('Authentication', () => {
await logoutMenuItem.click();
// Should redirect back to login
- await page.waitForURL(/\/login|\//, { timeout: TIMEOUTS.medium });
+ await page.waitForURL(/\/login|\//, { timeout: TIMEOUTS.navigation });
await expect(page.locator('input[name="username"]')).toBeVisible();
});
test.skip('should persist session on page reload', async ({ page }) => {
- // Login first
- await page.goto('/');
- await page.fill('input[name="username"]', 'admin');
- await page.fill('input[name="password"]', 'readur2024');
- await page.click('button[type="submit"]');
+ const authHelper = new AuthHelper(page);
- await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.medium });
+ // Login first
+ await authHelper.loginAs(TEST_CREDENTIALS.admin);
+
+ await page.waitForURL(/\/dashboard|\//, { timeout: TIMEOUTS.navigation });
// Reload the page
await page.reload();
@@ -93,7 +79,7 @@ test.describe('Authentication', () => {
await page.waitForLoadState('networkidle');
// Should still be logged in (either on dashboard or main page, but not login)
- await page.waitForURL(/\/dashboard|\/(?!login)/, { timeout: TIMEOUTS.medium });
+ await page.waitForURL(/\/dashboard|\/(?!login)/, { timeout: TIMEOUTS.navigation });
await expect(page.locator('input[name="username"]')).not.toBeVisible();
});
diff --git a/frontend/e2e/sources.spec.ts b/frontend/e2e/sources.spec.ts
index e8f924e..1f27f42 100644
--- a/frontend/e2e/sources.spec.ts
+++ b/frontend/e2e/sources.spec.ts
@@ -5,18 +5,18 @@ import { TestHelpers } from './utils/test-helpers';
test.describe('Source Management', () => {
let helpers: TestHelpers;
- test.beforeEach(async ({ authenticatedPage }) => {
- helpers = new TestHelpers(authenticatedPage);
+ test.beforeEach(async ({ adminPage }) => {
+ helpers = new TestHelpers(adminPage);
await helpers.navigateToPage('/sources');
});
- test.skip('should display sources interface', async ({ authenticatedPage: page }) => {
+ test.skip('should display sources interface', async ({ adminPage: page }) => {
// Check for sources page components
await expect(page.locator('[data-testid="sources-list"], .sources-list, .sources-container')).toBeVisible();
await expect(page.locator('button:has-text("Add Source"), [data-testid="add-source"]')).toBeVisible();
});
- test.skip('should create a new local folder source', async ({ authenticatedPage: page }) => {
+ test.skip('should create a new local folder source', async ({ adminPage: page }) => {
// Click add source button
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
@@ -51,7 +51,7 @@ test.describe('Source Management', () => {
await expect(page.locator(':has-text("Test Local Folder")')).toBeVisible({ timeout: TIMEOUTS.medium });
});
- test.skip('should create a new WebDAV source', async ({ authenticatedPage: page }) => {
+ test.skip('should create a new WebDAV source', async ({ adminPage: page }) => {
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal, .source-form')).toBeVisible();
@@ -79,7 +79,7 @@ test.describe('Source Management', () => {
await expect(page.locator(':has-text("Test WebDAV")')).toBeVisible({ timeout: TIMEOUTS.medium });
});
- test.skip('should create a new S3 source', async ({ authenticatedPage: page }) => {
+ test.skip('should create a new S3 source', async ({ adminPage: page }) => {
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal, .source-form')).toBeVisible();
@@ -108,7 +108,7 @@ test.describe('Source Management', () => {
await expect(page.locator(':has-text("Test S3 Bucket")')).toBeVisible({ timeout: TIMEOUTS.medium });
});
- test('should edit existing source', async ({ authenticatedPage: page }) => {
+ test('should edit existing source', async ({ adminPage: page }) => {
// Look for existing source to edit
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
@@ -138,7 +138,7 @@ test.describe('Source Management', () => {
}
});
- test('should delete source', async ({ authenticatedPage: page }) => {
+ test('should delete source', async ({ adminPage: page }) => {
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
if (await firstSource.isVisible()) {
@@ -168,7 +168,7 @@ test.describe('Source Management', () => {
}
});
- test.skip('should start source sync', async ({ authenticatedPage: page }) => {
+ test.skip('should start source sync', async ({ adminPage: page }) => {
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
if (await firstSource.isVisible()) {
@@ -189,7 +189,7 @@ test.describe('Source Management', () => {
}
});
- test('should stop source sync', async ({ authenticatedPage: page }) => {
+ test('should stop source sync', async ({ adminPage: page }) => {
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
if (await firstSource.isVisible()) {
@@ -217,7 +217,7 @@ test.describe('Source Management', () => {
}
});
- test('should display source status and statistics', async ({ authenticatedPage: page }) => {
+ test('should display source status and statistics', async ({ adminPage: page }) => {
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
if (await firstSource.isVisible()) {
@@ -235,7 +235,7 @@ test.describe('Source Management', () => {
}
});
- test.skip('should test source connection', async ({ authenticatedPage: page }) => {
+ test.skip('should test source connection', async ({ adminPage: page }) => {
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal')).toBeVisible();
@@ -266,7 +266,7 @@ test.describe('Source Management', () => {
}
});
- test('should filter sources by type', async ({ authenticatedPage: page }) => {
+ test('should filter sources by type', async ({ adminPage: page }) => {
// Look for filter dropdown
const filterDropdown = page.locator('[data-testid="source-filter"], select[name="filter"], .source-filter');
if (await filterDropdown.isVisible()) {
@@ -282,7 +282,7 @@ test.describe('Source Management', () => {
}
});
- test('should display sync history', async ({ authenticatedPage: page }) => {
+ test('should display sync history', async ({ adminPage: page }) => {
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
if (await firstSource.isVisible()) {
@@ -297,7 +297,7 @@ test.describe('Source Management', () => {
}
});
- test.skip('should validate required fields in source creation', async ({ authenticatedPage: page }) => {
+ test.skip('should validate required fields in source creation', async ({ adminPage: page }) => {
await page.click('button:has-text("Add Source"), [data-testid="add-source"]');
await expect(page.locator('[data-testid="add-source-form"], .add-source-modal')).toBeVisible();
@@ -316,7 +316,7 @@ test.describe('Source Management', () => {
}
});
- test('should schedule automatic sync', async ({ authenticatedPage: page }) => {
+ test('should schedule automatic sync', async ({ adminPage: page }) => {
const firstSource = page.locator('[data-testid="source-item"], .source-item, .source-card').first();
if (await firstSource.isVisible()) {
diff --git a/frontend/e2e/utils/test-data.ts b/frontend/e2e/utils/test-data.ts
index df71c00..bb836f8 100644
--- a/frontend/e2e/utils/test-data.ts
+++ b/frontend/e2e/utils/test-data.ts
@@ -1,8 +1,7 @@
+import { TEST_CREDENTIALS } from '../fixtures/auth';
+
export const TEST_USERS = {
- valid: {
- username: 'admin',
- password: 'readur2024'
- },
+ valid: TEST_CREDENTIALS.admin,
invalid: {
username: 'invaliduser',
password: 'wrongpassword'
diff --git a/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx b/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx
index 9232e0b..8e48bd5 100644
--- a/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx
+++ b/frontend/src/components/Auth/__tests__/Login.oidc.test.tsx
@@ -1,9 +1,7 @@
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { BrowserRouter } from 'react-router-dom';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
import { vi } from 'vitest';
import Login from '../Login';
-import { AuthProvider } from '../../../contexts/AuthContext';
-import { ThemeProvider } from '../../../contexts/ThemeContext';
+import { renderWithProviders, setupTestEnvironment } from '../../../test/test-utils';
// Mock the API
vi.mock('../../../services/api', () => ({
@@ -27,15 +25,6 @@ vi.mock('react-router-dom', async () => {
};
});
-// Mock localStorage
-Object.defineProperty(window, 'localStorage', {
- value: {
- setItem: vi.fn(),
- getItem: vi.fn(() => null),
- removeItem: vi.fn()
- }
-});
-
// Mock window.location
Object.defineProperty(window, 'location', {
value: {
@@ -44,58 +33,14 @@ Object.defineProperty(window, 'location', {
writable: true
});
-
-// Mock AuthContext
-const mockAuthContextValue = {
- user: null,
- loading: false,
- login: vi.fn(),
- register: vi.fn(),
- logout: vi.fn()
-};
-
-const MockAuthProvider = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-);
-
-const MockThemeProvider = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-);
-
describe('Login - OIDC Features', () => {
beforeEach(() => {
vi.clearAllMocks();
-
- // Mock window.matchMedia
- Object.defineProperty(window, 'matchMedia', {
- writable: true,
- value: vi.fn().mockImplementation(query => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: vi.fn(),
- removeListener: vi.fn(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- dispatchEvent: vi.fn(),
- })),
- });
+ setupTestEnvironment();
});
const renderLogin = () => {
- return render(
-
-
-
-
-
-
-
- );
+ return renderWithProviders();
};
it('renders OIDC login button', () => {
diff --git a/frontend/src/components/Auth/__tests__/OidcCallback.test.tsx b/frontend/src/components/Auth/__tests__/OidcCallback.test.tsx
index a2cf8e9..f54375a 100644
--- a/frontend/src/components/Auth/__tests__/OidcCallback.test.tsx
+++ b/frontend/src/components/Auth/__tests__/OidcCallback.test.tsx
@@ -1,8 +1,8 @@
-import { render, screen, waitFor, fireEvent } from '@testing-library/react';
+import { screen, waitFor, fireEvent } from '@testing-library/react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { vi } from 'vitest';
import OidcCallback from '../OidcCallback';
-import { AuthProvider } from '../../../contexts/AuthContext';
+import { renderWithProviders, setupTestEnvironment } from '../../../test/test-utils';
import { api } from '../../../services/api';
// Mock the API
@@ -28,15 +28,6 @@ vi.mock('react-router-dom', async () => {
};
});
-// Mock localStorage
-Object.defineProperty(window, 'localStorage', {
- value: {
- setItem: vi.fn(),
- getItem: vi.fn(),
- removeItem: vi.fn()
- }
-});
-
// Mock window.location
Object.defineProperty(window, 'location', {
value: {
@@ -45,56 +36,24 @@ Object.defineProperty(window, 'location', {
writable: true
});
-
-// Mock AuthContext
-const mockAuthContextValue = {
- user: null,
- loading: false,
- login: vi.fn(),
- register: vi.fn(),
- logout: vi.fn()
-};
-
-const MockAuthProvider = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-);
-
describe('OidcCallback', () => {
beforeEach(() => {
vi.clearAllMocks();
+ setupTestEnvironment();
window.location.href = '';
// Clear API mocks
(api.get as any).mockClear();
// Reset API mocks to default implementation
(api.get as any).mockResolvedValue({ data: { token: 'default-token' } });
-
- // Mock window.matchMedia
- Object.defineProperty(window, 'matchMedia', {
- writable: true,
- value: vi.fn().mockImplementation(query => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: vi.fn(),
- removeListener: vi.fn(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn(),
- dispatchEvent: vi.fn(),
- })),
- });
});
const renderOidcCallback = (search = '') => {
- return render(
+ return renderWithProviders(
-
-
- } />
- Login Page} />
-
-
+
+ } />
+ Login Page} />
+
);
};
diff --git a/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx b/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx
index 470ecd9..16d9e9e 100644
--- a/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx
+++ b/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx
@@ -3,10 +3,12 @@ import { screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { vi } from 'vitest';
import GlobalSearchBar from '../GlobalSearchBar';
-import { renderWithProviders, createMockApiServices } from '../../../test/test-utils';
+import { renderWithProviders, createMockApiServices, setupTestEnvironment, createMockLocalStorage } from '../../../test/test-utils';
// Use centralized API mocking
const mockServices = createMockApiServices();
+const mockDocumentService = mockServices.documentService;
+const localStorageMock = createMockLocalStorage();
// Mock useNavigate
const mockNavigate = vi.fn();
@@ -43,6 +45,7 @@ const mockSearchResponse = {
describe('GlobalSearchBar', () => {
beforeEach(() => {
vi.clearAllMocks();
+ setupTestEnvironment();
localStorageMock.getItem.mockReturnValue(null);
mockDocumentService.enhancedSearch.mockResolvedValue(mockSearchResponse);
});
diff --git a/frontend/src/components/Labels/__tests__/Label.test.tsx b/frontend/src/components/Labels/__tests__/Label.test.tsx
index 2213a60..ab82d83 100644
--- a/frontend/src/components/Labels/__tests__/Label.test.tsx
+++ b/frontend/src/components/Labels/__tests__/Label.test.tsx
@@ -1,32 +1,24 @@
import { describe, test, expect, vi, beforeEach } from 'vitest';
-import { render, screen, fireEvent } from '@testing-library/react';
-import { ThemeProvider, createTheme } from '@mui/material/styles';
+import { screen, fireEvent } from '@testing-library/react';
import Label, { type LabelData } from '../Label';
+import { renderWithProviders } from '../../../test/test-utils';
+import {
+ createMockLabel,
+ createMockSystemLabel,
+ setupTestEnvironment
+} from '../../../test/label-test-utils';
-const theme = createTheme();
-
-const mockLabel: LabelData = {
- id: 'test-label-1',
+const mockLabel = createMockLabel({
name: 'Test Label',
- description: 'A test label',
color: '#ff0000',
- background_color: undefined,
- icon: 'star',
- is_system: false,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
document_count: 5,
source_count: 2,
-};
+});
-const systemLabel: LabelData = {
- ...mockLabel,
- id: 'system-label-1',
+const systemLabel = createMockSystemLabel({
name: 'Important',
color: '#d73a49',
- icon: 'star',
- is_system: true,
-};
+});
const renderLabel = (props: Partial> = {}) => {
const defaultProps = {
@@ -34,14 +26,14 @@ const renderLabel = (props: Partial> = {}) =>
...props,
};
- return render(
-
-
-
- );
+ return renderWithProviders();
};
describe('Label Component', () => {
+ beforeEach(() => {
+ setupTestEnvironment();
+ });
+
describe('Basic Rendering', () => {
test('should render label with name', () => {
renderLabel();
@@ -116,7 +108,7 @@ describe('Label Component', () => {
const labelElement = screen.getByText('Test Label').closest('.MuiChip-root');
fireEvent.click(labelElement!);
- expect(handleClick).toHaveBeenCalledWith('test-label-1');
+ expect(handleClick).toHaveBeenCalledWith(mockLabel.id);
});
test('should not call onClick when disabled', () => {
@@ -166,7 +158,7 @@ describe('Label Component', () => {
const deleteButton = screen.getByTestId('CloseIcon');
fireEvent.click(deleteButton);
- expect(handleDelete).toHaveBeenCalledWith('test-label-1');
+ expect(handleDelete).toHaveBeenCalledWith(mockLabel.id);
});
test('should not call onDelete when disabled', () => {
@@ -196,7 +188,7 @@ describe('Label Component', () => {
const deleteButton = screen.getByTestId('CloseIcon');
fireEvent.click(deleteButton);
- expect(handleDelete).toHaveBeenCalledWith('test-label-1');
+ expect(handleDelete).toHaveBeenCalledWith(mockLabel.id);
expect(handleClick).not.toHaveBeenCalled();
});
});
@@ -285,7 +277,7 @@ describe('Label Component', () => {
// Test that clicking still works (keyboard events are handled internally by Material-UI)
fireEvent.click(labelElement!);
- expect(handleClick).toHaveBeenCalledWith('test-label-1');
+ expect(handleClick).toHaveBeenCalledWith(mockLabel.id);
});
test('should have proper disabled state attributes', () => {
diff --git a/frontend/src/components/Labels/__tests__/LabelCreateDialog.test.tsx b/frontend/src/components/Labels/__tests__/LabelCreateDialog.test.tsx
index bd646d4..6aebc27 100644
--- a/frontend/src/components/Labels/__tests__/LabelCreateDialog.test.tsx
+++ b/frontend/src/components/Labels/__tests__/LabelCreateDialog.test.tsx
@@ -1,25 +1,22 @@
import { describe, test, expect, vi, beforeEach } from 'vitest';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { ThemeProvider, createTheme } from '@mui/material/styles';
import LabelCreateDialog from '../LabelCreateDialog';
import { type LabelData } from '../Label';
+import { renderWithProviders } from '../../../test/test-utils';
+import {
+ createMockLabel,
+ setupTestEnvironment,
+ labelValidationScenarios
+} from '../../../test/label-test-utils';
-const theme = createTheme();
-
-const mockEditingLabel: LabelData = {
- id: 'edit-label-1',
+const mockEditingLabel = createMockLabel({
name: 'Existing Label',
description: 'An existing label',
color: '#ff0000',
- background_color: undefined,
- icon: 'star',
- is_system: false,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
document_count: 5,
source_count: 2,
-};
+});
const renderLabelCreateDialog = (props: Partial> = {}) => {
const defaultProps = {
@@ -29,17 +26,14 @@ const renderLabelCreateDialog = (props: Partial
-
-
- );
+ return renderWithProviders();
};
describe('LabelCreateDialog Component', () => {
let user: ReturnType;
beforeEach(() => {
+ setupTestEnvironment();
user = userEvent.setup();
});
diff --git a/frontend/src/components/Labels/__tests__/LabelSelector.test.tsx b/frontend/src/components/Labels/__tests__/LabelSelector.test.tsx
index 6d3ab89..d41c7f2 100644
--- a/frontend/src/components/Labels/__tests__/LabelSelector.test.tsx
+++ b/frontend/src/components/Labels/__tests__/LabelSelector.test.tsx
@@ -1,50 +1,15 @@
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
-import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { ThemeProvider, createTheme } from '@mui/material/styles';
import LabelSelector from '../LabelSelector';
import { type LabelData } from '../Label';
+import { renderWithProviders } from '../../../test/test-utils';
+import {
+ setupTestEnvironment,
+ testDataBuilders
+} from '../../../test/label-test-utils';
-const theme = createTheme();
-
-const mockLabels: LabelData[] = [
- {
- id: 'label-1',
- name: 'Important',
- description: 'High priority items',
- color: '#d73a49',
- icon: 'star',
- is_system: true,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
- document_count: 10,
- source_count: 2,
- },
- {
- id: 'label-2',
- name: 'Work',
- description: 'Work-related documents',
- color: '#0969da',
- icon: 'work',
- is_system: true,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
- document_count: 5,
- source_count: 1,
- },
- {
- id: 'label-3',
- name: 'Personal Project',
- description: 'My personal project files',
- color: '#28a745',
- icon: 'folder',
- is_system: false,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
- document_count: 3,
- source_count: 0,
- },
-];
+const mockLabels = testDataBuilders.createTypicalLabelSet();
const renderLabelSelector = (props: Partial> = {}) => {
const defaultProps = {
@@ -54,17 +19,14 @@ const renderLabelSelector = (props: Partial
-
-
- );
+ return renderWithProviders();
};
describe('LabelSelector Component', () => {
let user: ReturnType;
beforeEach(() => {
+ setupTestEnvironment();
user = userEvent.setup();
});
diff --git a/frontend/src/components/Upload/__tests__/UploadZone.test.tsx b/frontend/src/components/Upload/__tests__/UploadZone.test.tsx
index 4999766..d861c34 100644
--- a/frontend/src/components/Upload/__tests__/UploadZone.test.tsx
+++ b/frontend/src/components/Upload/__tests__/UploadZone.test.tsx
@@ -1,29 +1,25 @@
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
-import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
+import { screen, fireEvent, act, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import UploadZone from '../UploadZone';
-import { NotificationProvider } from '../../../contexts/NotificationContext';
+import { renderWithProviders, setupTestEnvironment, createMockApiServices } from '../../../test/test-utils';
+import { createMockLabel } from '../../../test/label-test-utils';
-// Mock axios directly
+// Setup centralized API mocks for this component
+const mockApiServices = createMockApiServices();
+
+// Mock axios directly with our mock labels
vi.mock('axios', () => ({
default: {
create: vi.fn(() => ({
get: vi.fn().mockResolvedValue({
status: 200,
- data: [
- {
- id: 'mock-label-1',
- name: 'Test Label',
- description: 'A test label',
- color: '#0969da',
- icon: undefined,
- is_system: false,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
- document_count: 0,
- source_count: 0,
- }
- ]
+ data: [createMockLabel({
+ name: 'Test Label',
+ color: '#0969da',
+ document_count: 0,
+ source_count: 0,
+ })]
}),
post: vi.fn().mockResolvedValue({ status: 201, data: {} }),
put: vi.fn().mockResolvedValue({ status: 200, data: {} }),
@@ -32,19 +28,6 @@ vi.mock('axios', () => ({
},
}));
-// Helper function to render with NotificationProvider
-const renderWithProvider = async (component: React.ReactElement) => {
- let renderResult;
- await act(async () => {
- renderResult = render(
-
- {component}
-
- );
- });
- return renderResult;
-};
-
const mockProps = {
onUploadComplete: vi.fn(),
};
@@ -54,6 +37,7 @@ describe('UploadZone', () => {
beforeEach(() => {
vi.clearAllMocks();
+ setupTestEnvironment();
// Suppress console.error for "Failed to fetch labels" during tests
originalConsoleError = console.error;
console.error = vi.fn().mockImplementation((message, ...args) => {
@@ -70,7 +54,9 @@ describe('UploadZone', () => {
});
test('renders upload zone with default text', async () => {
- await renderWithProvider();
+ await act(async () => {
+ renderWithProviders();
+ });
// Wait for async operations to complete
await waitFor(() => {
diff --git a/frontend/src/pages/__tests__/LabelsPage.test.tsx b/frontend/src/pages/__tests__/LabelsPage.test.tsx
index b0c8c42..8fdc173 100644
--- a/frontend/src/pages/__tests__/LabelsPage.test.tsx
+++ b/frontend/src/pages/__tests__/LabelsPage.test.tsx
@@ -1,74 +1,57 @@
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
-import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
+import { screen, waitFor, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { ThemeProvider, createTheme } from '@mui/material/styles';
-import { BrowserRouter } from 'react-router-dom';
import LabelsPage from '../LabelsPage';
import * as useApiModule from '../../hooks/useApi';
-
-const theme = createTheme();
+import { renderWithAuthenticatedUser, setupTestEnvironment } from '../../test/test-utils';\nimport { createMockLabel } from '../../test/label-test-utils';
const mockLabels = [
- {
+ createMockLabel({
id: 'label-1',
name: 'Important',
description: 'High priority items',
color: '#d73a49',
icon: 'star',
is_system: true,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
document_count: 10,
source_count: 2,
- },
- {
+ }),
+ createMockLabel({
id: 'label-2',
name: 'Work',
description: 'Work-related documents',
color: '#0969da',
icon: 'work',
is_system: true,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
document_count: 5,
source_count: 1,
- },
- {
+ }),
+ createMockLabel({
id: 'label-3',
name: 'Personal Project',
description: 'My personal project files',
color: '#28a745',
icon: 'folder',
is_system: false,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
document_count: 3,
source_count: 0,
- },
- {
+ }),
+ createMockLabel({
id: 'label-4',
name: 'Archive',
description: 'Archived items',
color: '#6e7781',
icon: 'archive',
is_system: true,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
document_count: 0,
source_count: 0,
- },
+ }),
];
const renderLabelsPage = async () => {
let renderResult;
await act(async () => {
- renderResult = render(
-
-
-
-
-
- );
+ renderResult = renderWithAuthenticatedUser();
});
return renderResult;
};
@@ -83,6 +66,7 @@ describe('LabelsPage Component', () => {
};
beforeEach(() => {
+ setupTestEnvironment();
user = userEvent.setup();
mockApi = {
@@ -412,16 +396,14 @@ describe('LabelsPage Component', () => {
});
test('should call API when creating new label', async () => {
- const newLabel = {
+ const newLabel = createMockLabel({
id: 'new-label',
name: 'New Label',
color: '#ff0000',
is_system: false,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
document_count: 0,
source_count: 0,
- };
+ });
mockApi.post.mockResolvedValue({ status: 201, data: newLabel });
@@ -604,16 +586,14 @@ describe('LabelsPage Component', () => {
describe('Data Refresh', () => {
test('should refresh labels after successful creation', async () => {
- const newLabel = {
+ const newLabel = createMockLabel({
id: 'new-label',
name: 'New Label',
color: '#ff0000',
is_system: false,
- created_at: '2024-01-01T00:00:00Z',
- updated_at: '2024-01-01T00:00:00Z',
document_count: 0,
source_count: 0,
- };
+ });
mockApi.post.mockResolvedValue({ status: 201, data: newLabel });
diff --git a/frontend/src/pages/__tests__/SearchPage.test.tsx b/frontend/src/pages/__tests__/SearchPage.test.tsx
index 8633278..6411270 100644
--- a/frontend/src/pages/__tests__/SearchPage.test.tsx
+++ b/frontend/src/pages/__tests__/SearchPage.test.tsx
@@ -1,7 +1,7 @@
import { describe, test, expect, vi, beforeEach } from 'vitest';
-import { render, screen } from '@testing-library/react';
-import { BrowserRouter } from 'react-router-dom';
+import { screen } from '@testing-library/react';
import SearchPage from '../SearchPage';
+import { renderWithAuthenticatedUser, createMockUser, setupTestEnvironment } from '../../test/test-utils';
// Mock API functions
vi.mock('../../services/api', () => ({
@@ -14,21 +14,14 @@ vi.mock('../../services/api', () => ({
getSettings: vi.fn(() => Promise.resolve({})),
}));
-const SearchPageWrapper = ({ children }: { children: React.ReactNode }) => {
- return {children};
-};
-
describe('SearchPage', () => {
beforeEach(() => {
vi.clearAllMocks();
+ setupTestEnvironment();
});
test('renders search page structure', () => {
- render(
-
-
-
- );
+ renderWithAuthenticatedUser();
// Check for page title
expect(screen.getByText('Search Documents')).toBeInTheDocument();
@@ -38,11 +31,7 @@ describe('SearchPage', () => {
});
test('renders search input', () => {
- render(
-
-
-
- );
+ renderWithAuthenticatedUser();
const searchInput = screen.getByPlaceholderText(/search/i);
expect(searchInput).toBeInTheDocument();
@@ -142,11 +131,7 @@ describe('SearchPage', () => {
// });
test('renders main search container', () => {
- const { container } = render(
-
-
-
- );
+ const { container } = renderWithAuthenticatedUser();
expect(container.firstChild).toBeInTheDocument();
});
diff --git a/frontend/src/services/__tests__/api.test.ts b/frontend/src/services/__tests__/api.test.ts
index 4763f33..ceef64a 100644
--- a/frontend/src/services/__tests__/api.test.ts
+++ b/frontend/src/services/__tests__/api.test.ts
@@ -1,31 +1,17 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { type OcrResponse, type Document } from '../api';
+import { createMockApiServices, setupTestEnvironment } from '../../test/test-utils';
-// Create mock functions for the documentService
-const mockGetOcrText = vi.fn();
-const mockList = vi.fn();
-const mockUpload = vi.fn();
-const mockDownload = vi.fn();
-const mockDeleteLowConfidence = vi.fn();
-const mockGetFailedOcrDocuments = vi.fn();
-const mockGetFailedDocuments = vi.fn();
-const mockRetryOcr = vi.fn();
+// Use centralized API mocking
+const mockServices = createMockApiServices();
+const mockDocumentService = mockServices.documentService;
-// Mock the entire api module
+// Mock the entire api module with centralized mocks
vi.mock('../api', async () => {
const actual = await vi.importActual('../api');
return {
...actual,
- documentService: {
- getOcrText: mockGetOcrText,
- list: mockList,
- upload: mockUpload,
- download: mockDownload,
- deleteLowConfidence: mockDeleteLowConfidence,
- getFailedOcrDocuments: mockGetFailedOcrDocuments,
- getFailedDocuments: mockGetFailedDocuments,
- retryOcr: mockRetryOcr,
- },
+ documentService: mockDocumentService,
};
});
@@ -35,6 +21,7 @@ const { documentService } = await import('../api');
describe('documentService', () => {
beforeEach(() => {
vi.clearAllMocks();
+ setupTestEnvironment();
});
describe('getOcrText', () => {
@@ -60,7 +47,7 @@ describe('documentService', () => {
config: {},
};
- mockGetOcrText.mockResolvedValue(mockResponse);
+ mockDocumentService.getOcrText.mockResolvedValue(mockResponse);
const result = await documentService.getOcrText('doc-123');
@@ -94,7 +81,7 @@ describe('documentService', () => {
config: {},
};
- mockGetOcrText.mockResolvedValue(mockResponse);
+ mockDocumentService.getOcrText.mockResolvedValue(mockResponse);
const result = await documentService.getOcrText('doc-456');
@@ -126,7 +113,7 @@ describe('documentService', () => {
config: {},
};
- mockGetOcrText.mockResolvedValue(mockResponse);
+ mockDocumentService.getOcrText.mockResolvedValue(mockResponse);
const result = await documentService.getOcrText('doc-789');
diff --git a/src/tests/db_tests.rs b/src/tests/db_tests.rs
index dca299c..8caad94 100644
--- a/src/tests/db_tests.rs
+++ b/src/tests/db_tests.rs
@@ -1,24 +1,10 @@
#[cfg(test)]
mod tests {
- use crate::db::Database;
+ use crate::test_utils::TestContext;
use crate::models::{CreateUser, Document, SearchRequest};
use chrono::Utc;
use uuid::Uuid;
- async fn create_test_db() -> Database {
- // Use an in-memory database URL for testing
- // This will require PostgreSQL to be running for integration tests
- let db_url = std::env::var("TEST_DATABASE_URL")
- .unwrap_or_else(|_| "postgresql://postgres:postgres@localhost:5432/readur_test".to_string());
-
- let db = Database::new(&db_url).await.expect("Failed to connect to test database");
-
- // Run migrations for test database
- db.migrate().await.expect("Failed to migrate test database");
-
- db
- }
-
fn create_test_user_data(suffix: &str) -> CreateUser {
CreateUser {
username: format!("testuser_{}", suffix),
@@ -58,9 +44,9 @@ mod tests {
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_create_user() {
- let db = create_test_db().await;
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
let user_data = create_test_user_data("1");
let result = db.create_user(user_data).await;
@@ -68,15 +54,15 @@ mod tests {
let user = result.unwrap();
assert_eq!(user.username, "testuser_1");
- assert_eq!(user.email, "test@example.com");
+ assert_eq!(user.email, "test_1@example.com");
assert!(user.password_hash.is_some());
assert_ne!(user.password_hash.as_ref().unwrap(), "password123"); // Should be hashed
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_get_user_by_username() {
- let db = create_test_db().await;
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
let user_data = create_test_user_data("1");
let created_user = db.create_user(user_data).await.unwrap();
@@ -93,9 +79,9 @@ mod tests {
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_get_user_by_username_not_found() {
- let db = create_test_db().await;
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
let result = db.get_user_by_username("nonexistent").await;
assert!(result.is_ok());
@@ -105,9 +91,9 @@ mod tests {
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_create_document() {
- let db = create_test_db().await;
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
let user_data = create_test_user_data("1");
let user = db.create_user(user_data).await.unwrap();
@@ -122,9 +108,9 @@ mod tests {
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_get_documents_by_user() {
- let db = create_test_db().await;
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
let user_data = create_test_user_data("1");
let user = db.create_user(user_data).await.unwrap();
@@ -142,9 +128,9 @@ mod tests {
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_search_documents() {
- let db = create_test_db().await;
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
let user_data = create_test_user_data("1");
let user = db.create_user(user_data).await.unwrap();
@@ -174,9 +160,9 @@ mod tests {
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_update_document_ocr() {
- let db = create_test_db().await;
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
let user_data = create_test_user_data("1");
let user = db.create_user(user_data).await.unwrap();
diff --git a/src/tests/documents_tests.rs b/src/tests/documents_tests.rs
index 9973974..efa5fc8 100644
--- a/src/tests/documents_tests.rs
+++ b/src/tests/documents_tests.rs
@@ -330,100 +330,29 @@ mod tests {
#[cfg(test)]
mod document_deletion_tests {
use super::*;
- use crate::db::Database;
- use crate::models::{UserRole, User, Document, AuthProvider};
+ use crate::test_utils::TestContext;
+ use crate::models::{UserRole, User, Document, AuthProvider, CreateUser};
use chrono::Utc;
- use sqlx::PgPool;
- use std::env;
use uuid::Uuid;
- async fn create_test_db_pool() -> PgPool {
- let database_url = env::var("TEST_DATABASE_URL")
- .expect("TEST_DATABASE_URL must be set for database tests");
- PgPool::connect(&database_url)
- .await
- .expect("Failed to connect to test database")
- }
-
- async fn create_test_user(pool: &PgPool, role: UserRole) -> User {
- let user_id = Uuid::new_v4();
- let user = User {
- id: user_id,
- username: format!("testuser_{}", user_id),
- email: format!("test_{}@example.com", user_id),
- password_hash: Some("hashed_password".to_string()),
- role,
- created_at: Utc::now(),
- updated_at: Utc::now(),
- oidc_subject: None,
- oidc_issuer: None,
- oidc_email: None,
- auth_provider: AuthProvider::Local,
- };
-
- // Insert user into database
- sqlx::query("INSERT INTO users (id, username, email, password_hash, role, created_at, updated_at, oidc_subject, oidc_issuer, oidc_email, auth_provider) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)")
- .bind(user.id)
- .bind(&user.username)
- .bind(&user.email)
- .bind(&user.password_hash)
- .bind(user.role.to_string())
- .bind(user.created_at)
- .bind(user.updated_at)
- .bind(&user.oidc_subject)
- .bind(&user.oidc_issuer)
- .bind(&user.oidc_email)
- .bind(user.auth_provider.to_string())
- .execute(pool)
- .await
- .expect("Failed to insert test user");
-
- user
- }
-
- async fn create_and_insert_test_document(pool: &PgPool, user_id: Uuid) -> Document {
- let document = super::create_test_document(user_id);
-
- // Insert document into database
- sqlx::query("INSERT INTO documents (id, filename, original_filename, file_path, file_size, mime_type, content, ocr_text, ocr_confidence, ocr_word_count, ocr_processing_time_ms, ocr_status, ocr_error, ocr_completed_at, tags, created_at, updated_at, user_id, file_hash) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)")
- .bind(document.id)
- .bind(&document.filename)
- .bind(&document.original_filename)
- .bind(&document.file_path)
- .bind(document.file_size as i64)
- .bind(&document.mime_type)
- .bind(&document.content)
- .bind(&document.ocr_text)
- .bind(document.ocr_confidence)
- .bind(document.ocr_word_count.map(|x| x as i32))
- .bind(document.ocr_processing_time_ms.map(|x| x as i32))
- .bind(&document.ocr_status)
- .bind(&document.ocr_error)
- .bind(document.ocr_completed_at)
- .bind(&document.tags)
- .bind(document.created_at)
- .bind(document.updated_at)
- .bind(document.user_id)
- .bind(&document.file_hash)
- .execute(pool)
- .await
- .expect("Failed to insert test document");
-
- document
- }
-
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_delete_document_as_owner() {
- let pool = create_test_db_pool().await;
- let documents_db = Database { pool: pool.clone() };
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
// Create test user and document
- let user = create_test_user(&pool, UserRole::User).await;
- let document = create_and_insert_test_document(&pool, user.id).await;
+ let user_data = CreateUser {
+ username: format!("testuser_{}", Uuid::new_v4()),
+ email: format!("test_{}@example.com", Uuid::new_v4()),
+ password: "password123".to_string(),
+ role: Some(UserRole::User),
+ };
+ let user = db.create_user(user_data).await.expect("Failed to create user");
+ let document = super::create_test_document(user.id);
+ let document = db.create_document(document).await.expect("Failed to create document");
// Delete document as owner
- let result = documents_db
+ let result = db
.delete_document(document.id, user.id, user.role)
.await
.expect("Failed to delete document");
@@ -435,7 +364,7 @@ mod document_deletion_tests {
assert_eq!(deleted_doc.user_id, user.id);
// Verify document no longer exists in database
- let found_doc = documents_db
+ let found_doc = db
.get_document_by_id(document.id, user.id, user.role)
.await
.expect("Database query failed");
@@ -443,20 +372,32 @@ mod document_deletion_tests {
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_delete_document_as_admin() {
- let pool = create_test_db_pool().await;
- let documents_db = Database { pool: pool.clone() };
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
// Create regular user and their document
- let user = create_test_user(&pool, UserRole::User).await;
- let document = create_and_insert_test_document(&pool, user.id).await;
+ let user_data = CreateUser {
+ username: format!("testuser_{}", Uuid::new_v4()),
+ email: format!("test_{}@example.com", Uuid::new_v4()),
+ password: "password123".to_string(),
+ role: Some(UserRole::User),
+ };
+ let user = db.create_user(user_data).await.expect("Failed to create user");
+ let document = super::create_test_document(user.id);
+ let document = db.create_document(document).await.expect("Failed to create document");
// Create admin user
- let admin = create_test_user(&pool, UserRole::Admin).await;
+ let admin_data = CreateUser {
+ username: format!("adminuser_{}", Uuid::new_v4()),
+ email: format!("admin_{}@example.com", Uuid::new_v4()),
+ password: "adminpass123".to_string(),
+ role: Some(UserRole::Admin),
+ };
+ let admin = db.create_user(admin_data).await.expect("Failed to create admin");
// Delete document as admin
- let result = documents_db
+ let result = db
.delete_document(document.id, admin.id, admin.role)
.await
.expect("Failed to delete document as admin");
@@ -469,20 +410,33 @@ mod document_deletion_tests {
}
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_delete_document_unauthorized() {
- let pool = create_test_db_pool().await;
- let documents_db = Database { pool: pool.clone() };
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
// Create two regular users
- let user1 = create_test_user(&pool, UserRole::User).await;
- let user2 = create_test_user(&pool, UserRole::User).await;
+ let user1_data = CreateUser {
+ username: format!("testuser1_{}", Uuid::new_v4()),
+ email: format!("test1_{}@example.com", Uuid::new_v4()),
+ password: "password123".to_string(),
+ role: Some(UserRole::User),
+ };
+ let user1 = db.create_user(user1_data).await.expect("Failed to create user1");
+
+ let user2_data = CreateUser {
+ username: format!("testuser2_{}", Uuid::new_v4()),
+ email: format!("test2_{}@example.com", Uuid::new_v4()),
+ password: "password123".to_string(),
+ role: Some(UserRole::User),
+ };
+ let user2 = db.create_user(user2_data).await.expect("Failed to create user2");
// Create document owned by user1
- let document = create_and_insert_test_document(&pool, user1.id).await;
+ let document = super::create_test_document(user1.id);
+ let document = db.create_document(document).await.expect("Failed to create document");
// Try to delete document as user2 (should fail)
- let result = documents_db
+ let result = db
.delete_document(document.id, user2.id, user2.role)
.await
.expect("Database query failed");
@@ -491,7 +445,7 @@ mod document_deletion_tests {
assert!(result.is_none());
// Verify document still exists
- let found_doc = documents_db
+ let found_doc = db
.get_document_by_id(document.id, user1.id, user1.role)
.await
.expect("Database query failed");
@@ -707,97 +661,27 @@ mod document_deletion_tests {
#[cfg(test)]
mod rbac_deletion_tests {
use super::*;
- use crate::db::Database;
- use crate::models::{UserRole, User, Document, AuthProvider};
- use chrono::Utc;
- use sqlx::PgPool;
- use std::env;
+ use crate::test_utils::TestContext;
+ use crate::models::{UserRole, CreateUser};
use uuid::Uuid;
- async fn create_test_db_pool() -> PgPool {
- let database_url = env::var("TEST_DATABASE_URL")
- .expect("TEST_DATABASE_URL must be set for database tests");
- PgPool::connect(&database_url)
- .await
- .expect("Failed to connect to test database")
- }
-
- async fn create_test_user(pool: &PgPool, role: UserRole) -> User {
- let user_id = Uuid::new_v4();
- let user = User {
- id: user_id,
- username: format!("testuser_{}", user_id),
- email: format!("test_{}@example.com", user_id),
- password_hash: Some("hashed_password".to_string()),
- role,
- created_at: Utc::now(),
- updated_at: Utc::now(),
- oidc_subject: None,
- oidc_issuer: None,
- oidc_email: None,
- auth_provider: AuthProvider::Local,
- };
-
- sqlx::query("INSERT INTO users (id, username, email, password_hash, role, created_at, updated_at, oidc_subject, oidc_issuer, oidc_email, auth_provider) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)")
- .bind(user.id)
- .bind(&user.username)
- .bind(&user.email)
- .bind(&user.password_hash)
- .bind(user.role.to_string())
- .bind(user.created_at)
- .bind(user.updated_at)
- .bind(&user.oidc_subject)
- .bind(&user.oidc_issuer)
- .bind(&user.oidc_email)
- .bind(user.auth_provider.to_string())
- .execute(pool)
- .await
- .expect("Failed to insert test user");
-
- user
- }
-
- async fn create_and_insert_test_document(pool: &PgPool, user_id: Uuid) -> Document {
- let document = super::create_test_document(user_id);
-
- sqlx::query("INSERT INTO documents (id, filename, original_filename, file_path, file_size, mime_type, content, ocr_text, ocr_confidence, ocr_word_count, ocr_processing_time_ms, ocr_status, ocr_error, ocr_completed_at, tags, created_at, updated_at, user_id, file_hash) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)")
- .bind(document.id)
- .bind(&document.filename)
- .bind(&document.original_filename)
- .bind(&document.file_path)
- .bind(document.file_size as i64)
- .bind(&document.mime_type)
- .bind(&document.content)
- .bind(&document.ocr_text)
- .bind(document.ocr_confidence)
- .bind(document.ocr_word_count.map(|x| x as i32))
- .bind(document.ocr_processing_time_ms.map(|x| x as i32))
- .bind(&document.ocr_status)
- .bind(&document.ocr_error)
- .bind(document.ocr_completed_at)
- .bind(&document.tags)
- .bind(document.created_at)
- .bind(document.updated_at)
- .bind(document.user_id)
- .bind(&document.file_hash)
- .execute(pool)
- .await
- .expect("Failed to insert test document");
-
- document
- }
-
#[tokio::test]
- #[ignore = "Requires PostgreSQL database"]
async fn test_user_can_delete_own_document() {
- let pool = create_test_db_pool().await;
- let documents_db = Database { pool: pool.clone() };
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
- let user = create_test_user(&pool, UserRole::User).await;
- let document = create_and_insert_test_document(&pool, user.id).await;
+ let user_data = CreateUser {
+ username: format!("testuser_{}", Uuid::new_v4()),
+ email: format!("test_{}@example.com", Uuid::new_v4()),
+ password: "password123".to_string(),
+ role: Some(UserRole::User),
+ };
+ let user = db.create_user(user_data).await.expect("Failed to create user");
+ let document = super::create_test_document(user.id);
+ let document = db.create_document(document).await.expect("Failed to create document");
// User should be able to delete their own document
- let result = documents_db
+ let result = db
.delete_document(document.id, user.id, user.role)
.await
.expect("Failed to delete document");
diff --git a/src/tests/enhanced_search_tests.rs b/src/tests/enhanced_search_tests.rs
index 4da7afd..3251038 100644
--- a/src/tests/enhanced_search_tests.rs
+++ b/src/tests/enhanced_search_tests.rs
@@ -901,22 +901,11 @@ mod tests {
#[tokio::test]
#[ignore = "Requires PostgreSQL database for integration testing"]
async fn test_enhanced_search_integration() {
- // This would test the actual database integration
- // Similar to existing db_tests but for enhanced search
- let db_url = std::env::var("TEST_DATABASE_URL")
- .unwrap_or_else(|_| "postgresql://postgres:postgres@localhost:5432/readur_test".to_string());
+ use crate::test_utils::{TestContext, TestAuthHelper};
- let db = Database::new(&db_url).await.expect("Failed to connect to test database");
- db.migrate().await.expect("Failed to migrate test database");
-
- // Create test user
- let user_data = CreateUser {
- username: "test_enhanced_search".to_string(),
- email: "enhanced@test.com".to_string(),
- password: "password123".to_string(),
- role: Some(crate::models::UserRole::User),
- };
- let user = db.create_user(user_data).await.unwrap();
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create test document with rich content
let document = Document {
@@ -937,7 +926,7 @@ mod tests {
tags: vec!["enhanced".to_string(), "search".to_string(), "test".to_string()],
created_at: Utc::now(),
updated_at: Utc::now(),
- user_id: user.id,
+ user_id: user.user_response.id,
file_hash: Some("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string()),
original_created_at: None,
original_modified_at: None,
@@ -946,7 +935,7 @@ mod tests {
ocr_failure_reason: None,
};
- db.create_document(document).await.unwrap();
+ ctx.state.db.create_document(document).await.unwrap();
// Test enhanced search with snippets
let search_request = SearchRequest {
@@ -960,7 +949,7 @@ mod tests {
search_mode: Some(SearchMode::Simple),
};
- let result = db.enhanced_search_documents(user.id, search_request).await;
+ let result = ctx.state.db.enhanced_search_documents(user.user_response.id, search_request).await;
assert!(result.is_ok());
let (documents, total, query_time) = result.unwrap();
diff --git a/src/tests/ignored_files_tests.rs b/src/tests/ignored_files_tests.rs
index dc93cc9..4ad6926 100644
--- a/src/tests/ignored_files_tests.rs
+++ b/src/tests/ignored_files_tests.rs
@@ -6,63 +6,199 @@ mod tests {
create_ignored_file_from_document
};
use crate::models::{CreateIgnoredFile, IgnoredFilesQuery, User, UserRole, Document, AuthProvider};
+ use crate::test_utils::{TestContext, TestAuthHelper};
use uuid::Uuid;
use chrono::Utc;
- use sqlx::PgPool;
- use std::env;
- async fn create_test_db_pool() -> PgPool {
- let database_url = env::var("TEST_DATABASE_URL")
- .or_else(|_| env::var("DATABASE_URL"))
- .unwrap_or_else(|_| {
- // Skip tests if no database URL is available
- println!("Skipping database tests: TEST_DATABASE_URL or DATABASE_URL not set");
- std::process::exit(0);
- });
- PgPool::connect(&database_url)
- .await
- .expect("Failed to connect to test database")
- }
- async fn create_test_user(pool: &PgPool) -> User {
- let user_id = Uuid::new_v4();
- let user = User {
- id: user_id,
- username: format!("testuser_{}", user_id),
- email: format!("test_{}@example.com", user_id),
- password_hash: Some("hashed_password".to_string()),
- role: UserRole::User,
- created_at: Utc::now(),
- updated_at: Utc::now(),
- oidc_subject: None,
- oidc_issuer: None,
- oidc_email: None,
- auth_provider: AuthProvider::Local,
+ #[tokio::test]
+ async fn test_create_ignored_file() {
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
+
+ let ignored_file = CreateIgnoredFile {
+ file_hash: "abc123".to_string(),
+ filename: "test.pdf".to_string(),
+ original_filename: "original_test.pdf".to_string(),
+ file_path: "/path/to/test.pdf".to_string(),
+ file_size: 1024,
+ mime_type: "application/pdf".to_string(),
+ source_type: Some("webdav".to_string()),
+ source_path: Some("/webdav/test.pdf".to_string()),
+ source_identifier: Some("webdav-server-1".to_string()),
+ ignored_by: user.user_response.id,
+ reason: Some("deleted by user".to_string()),
};
- sqlx::query("INSERT INTO users (id, username, email, password_hash, role, created_at, updated_at, oidc_subject, oidc_issuer, oidc_email, auth_provider) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)")
- .bind(user.id)
- .bind(&user.username)
- .bind(&user.email)
- .bind(&user.password_hash)
- .bind(user.role.to_string())
- .bind(user.created_at)
- .bind(user.updated_at)
- .bind(&user.oidc_subject)
- .bind(&user.oidc_issuer)
- .bind(&user.oidc_email)
- .bind(user.auth_provider.to_string())
- .execute(pool)
- .await
- .expect("Failed to insert test user");
+ let result = create_ignored_file(&ctx.state.db.pool, ignored_file).await;
+ assert!(result.is_ok());
- user
+ let created = result.unwrap();
+ assert_eq!(created.file_hash, "abc123");
+ assert_eq!(created.filename, "test.pdf");
+ assert_eq!(created.ignored_by, user.user_response.id);
+ assert_eq!(created.source_type, Some("webdav".to_string()));
}
- async fn create_test_document(pool: &PgPool, user_id: Uuid) -> Document {
- let document_id = Uuid::new_v4();
- let document = Document {
- id: document_id,
+ #[tokio::test]
+ async fn test_list_ignored_files() {
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
+
+ // Create multiple ignored files
+ for i in 0..3 {
+ let ignored_file = CreateIgnoredFile {
+ file_hash: format!("hash{}", i),
+ filename: format!("test{}.pdf", i),
+ original_filename: format!("original_test{}.pdf", i),
+ file_path: format!("/path/to/test{}.pdf", i),
+ file_size: 1024 * (i + 1) as i64,
+ mime_type: "application/pdf".to_string(),
+ source_type: Some("webdav".to_string()),
+ source_path: Some(format!("/webdav/test{}.pdf", i)),
+ source_identifier: Some("webdav-server-1".to_string()),
+ ignored_by: user.user_response.id,
+ reason: Some("deleted by user".to_string()),
+ };
+
+ create_ignored_file(&ctx.state.db.pool, ignored_file).await.unwrap();
+ }
+
+ let query = IgnoredFilesQuery {
+ limit: Some(10),
+ offset: Some(0),
+ source_type: None,
+ source_identifier: None,
+ ignored_by: None,
+ filename: None,
+ };
+
+ let result = list_ignored_files(&ctx.state.db.pool, user.user_response.id, &query).await;
+ assert!(result.is_ok());
+
+ let ignored_files = result.unwrap();
+ assert_eq!(ignored_files.len(), 3);
+ assert!(ignored_files.iter().all(|f| f.ignored_by == user.user_response.id));
+ }
+
+ #[tokio::test]
+ async fn test_get_ignored_file_by_id() {
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
+
+ let ignored_file = CreateIgnoredFile {
+ file_hash: "test_hash".to_string(),
+ filename: "test.pdf".to_string(),
+ original_filename: "original_test.pdf".to_string(),
+ file_path: "/path/to/test.pdf".to_string(),
+ file_size: 1024,
+ mime_type: "application/pdf".to_string(),
+ source_type: Some("webdav".to_string()),
+ source_path: Some("/webdav/test.pdf".to_string()),
+ source_identifier: Some("webdav-server-1".to_string()),
+ ignored_by: user.user_response.id,
+ reason: Some("deleted by user".to_string()),
+ };
+
+ let created = create_ignored_file(&ctx.state.db.pool, ignored_file).await.unwrap();
+
+ let result = get_ignored_file_by_id(&ctx.state.db.pool, created.id, user.user_response.id).await;
+ assert!(result.is_ok());
+
+ let fetched = result.unwrap();
+ assert!(fetched.is_some());
+
+ let fetched = fetched.unwrap();
+ assert_eq!(fetched.id, created.id);
+ assert_eq!(fetched.file_hash, "test_hash");
+ assert_eq!(fetched.filename, "test.pdf");
+ }
+
+ #[tokio::test]
+ async fn test_delete_ignored_file() {
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
+
+ let ignored_file = CreateIgnoredFile {
+ file_hash: "test_hash".to_string(),
+ filename: "test.pdf".to_string(),
+ original_filename: "original_test.pdf".to_string(),
+ file_path: "/path/to/test.pdf".to_string(),
+ file_size: 1024,
+ mime_type: "application/pdf".to_string(),
+ source_type: Some("webdav".to_string()),
+ source_path: Some("/webdav/test.pdf".to_string()),
+ source_identifier: Some("webdav-server-1".to_string()),
+ ignored_by: user.user_response.id,
+ reason: Some("deleted by user".to_string()),
+ };
+
+ let created = create_ignored_file(&ctx.state.db.pool, ignored_file).await.unwrap();
+
+ let result = delete_ignored_file(&ctx.state.db.pool, created.id, user.user_response.id).await;
+ assert!(result.is_ok());
+ assert!(result.unwrap());
+
+ // Verify it's deleted
+ let fetched = get_ignored_file_by_id(&ctx.state.db.pool, created.id, user.user_response.id).await;
+ assert!(fetched.is_ok());
+ assert!(fetched.unwrap().is_none());
+ }
+
+ #[tokio::test]
+ async fn test_is_file_ignored() {
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
+
+ let ignored_file = CreateIgnoredFile {
+ file_hash: "test_hash".to_string(),
+ filename: "test.pdf".to_string(),
+ original_filename: "original_test.pdf".to_string(),
+ file_path: "/path/to/test.pdf".to_string(),
+ file_size: 1024,
+ mime_type: "application/pdf".to_string(),
+ source_type: Some("webdav".to_string()),
+ source_path: Some("/webdav/test.pdf".to_string()),
+ source_identifier: Some("webdav-server-1".to_string()),
+ ignored_by: user.user_response.id,
+ reason: Some("deleted by user".to_string()),
+ };
+
+ create_ignored_file(&ctx.state.db.pool, ignored_file).await.unwrap();
+
+ // Test with exact match
+ let result = is_file_ignored(
+ &ctx.state.db.pool,
+ "test_hash",
+ Some("webdav"),
+ Some("/webdav/test.pdf")
+ ).await;
+ assert!(result.is_ok());
+ assert!(result.unwrap());
+
+ // Test with just hash
+ let result = is_file_ignored(&ctx.state.db.pool, "test_hash", None, None).await;
+ assert!(result.is_ok());
+ assert!(result.unwrap());
+
+ // Test with non-existing hash
+ let result = is_file_ignored(&ctx.state.db.pool, "non_existing", None, None).await;
+ assert!(result.is_ok());
+ assert!(!result.unwrap());
+ }
+
+ #[tokio::test]
+ async fn test_create_ignored_file_from_document() {
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
+ let document = ctx.state.db.create_document(crate::models::Document {
+ id: Uuid::new_v4(),
filename: "test_document.pdf".to_string(),
original_filename: "test_document.pdf".to_string(),
file_path: "/uploads/test_document.pdf".to_string(),
@@ -79,228 +215,19 @@ mod tests {
tags: vec!["test".to_string(), "document".to_string()],
created_at: Utc::now(),
updated_at: Utc::now(),
- user_id,
+ user_id: user.user_response.id,
file_hash: Some("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string()),
original_created_at: None,
original_modified_at: None,
source_metadata: None,
ocr_retry_count: None,
ocr_failure_reason: None,
- };
-
- sqlx::query("INSERT INTO documents (id, filename, original_filename, file_path, file_size, mime_type, content, ocr_text, ocr_confidence, ocr_word_count, ocr_processing_time_ms, ocr_status, ocr_error, ocr_completed_at, tags, created_at, updated_at, user_id, file_hash) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)")
- .bind(document.id)
- .bind(&document.filename)
- .bind(&document.original_filename)
- .bind(&document.file_path)
- .bind(document.file_size as i64)
- .bind(&document.mime_type)
- .bind(&document.content)
- .bind(&document.ocr_text)
- .bind(document.ocr_confidence)
- .bind(document.ocr_word_count.map(|x| x as i32))
- .bind(document.ocr_processing_time_ms.map(|x| x as i32))
- .bind(&document.ocr_status)
- .bind(&document.ocr_error)
- .bind(document.ocr_completed_at)
- .bind(&document.tags)
- .bind(document.created_at)
- .bind(document.updated_at)
- .bind(document.user_id)
- .bind(&document.file_hash)
- .execute(pool)
- .await
- .expect("Failed to insert test document");
-
- document
- }
-
- #[tokio::test]
- async fn test_create_ignored_file() {
- let pool = create_test_db_pool().await;
- let user = create_test_user(&pool).await;
-
- let ignored_file = CreateIgnoredFile {
- file_hash: "abc123".to_string(),
- filename: "test.pdf".to_string(),
- original_filename: "original_test.pdf".to_string(),
- file_path: "/path/to/test.pdf".to_string(),
- file_size: 1024,
- mime_type: "application/pdf".to_string(),
- source_type: Some("webdav".to_string()),
- source_path: Some("/webdav/test.pdf".to_string()),
- source_identifier: Some("webdav-server-1".to_string()),
- ignored_by: user.id,
- reason: Some("deleted by user".to_string()),
- };
-
- let result = create_ignored_file(&pool, ignored_file).await;
- assert!(result.is_ok());
-
- let created = result.unwrap();
- assert_eq!(created.file_hash, "abc123");
- assert_eq!(created.filename, "test.pdf");
- assert_eq!(created.ignored_by, user.id);
- assert_eq!(created.source_type, Some("webdav".to_string()));
- }
-
- #[tokio::test]
- async fn test_list_ignored_files() {
- let pool = create_test_db_pool().await;
- let user = create_test_user(&pool).await;
-
- // Create multiple ignored files
- for i in 0..3 {
- let ignored_file = CreateIgnoredFile {
- file_hash: format!("hash{}", i),
- filename: format!("test{}.pdf", i),
- original_filename: format!("original_test{}.pdf", i),
- file_path: format!("/path/to/test{}.pdf", i),
- file_size: 1024 * (i + 1) as i64,
- mime_type: "application/pdf".to_string(),
- source_type: Some("webdav".to_string()),
- source_path: Some(format!("/webdav/test{}.pdf", i)),
- source_identifier: Some("webdav-server-1".to_string()),
- ignored_by: user.id,
- reason: Some("deleted by user".to_string()),
- };
-
- create_ignored_file(&pool, ignored_file).await.unwrap();
- }
-
- let query = IgnoredFilesQuery {
- limit: Some(10),
- offset: Some(0),
- source_type: None,
- source_identifier: None,
- ignored_by: None,
- filename: None,
- };
-
- let result = list_ignored_files(&pool, user.id, &query).await;
- assert!(result.is_ok());
-
- let ignored_files = result.unwrap();
- assert_eq!(ignored_files.len(), 3);
- assert!(ignored_files.iter().all(|f| f.ignored_by == user.id));
- }
-
- #[tokio::test]
- async fn test_get_ignored_file_by_id() {
- let pool = create_test_db_pool().await;
- let user = create_test_user(&pool).await;
-
- let ignored_file = CreateIgnoredFile {
- file_hash: "test_hash".to_string(),
- filename: "test.pdf".to_string(),
- original_filename: "original_test.pdf".to_string(),
- file_path: "/path/to/test.pdf".to_string(),
- file_size: 1024,
- mime_type: "application/pdf".to_string(),
- source_type: Some("webdav".to_string()),
- source_path: Some("/webdav/test.pdf".to_string()),
- source_identifier: Some("webdav-server-1".to_string()),
- ignored_by: user.id,
- reason: Some("deleted by user".to_string()),
- };
-
- let created = create_ignored_file(&pool, ignored_file).await.unwrap();
-
- let result = get_ignored_file_by_id(&pool, created.id, user.id).await;
- assert!(result.is_ok());
-
- let fetched = result.unwrap();
- assert!(fetched.is_some());
-
- let fetched = fetched.unwrap();
- assert_eq!(fetched.id, created.id);
- assert_eq!(fetched.file_hash, "test_hash");
- assert_eq!(fetched.filename, "test.pdf");
- }
-
- #[tokio::test]
- async fn test_delete_ignored_file() {
- let pool = create_test_db_pool().await;
- let user = create_test_user(&pool).await;
-
- let ignored_file = CreateIgnoredFile {
- file_hash: "test_hash".to_string(),
- filename: "test.pdf".to_string(),
- original_filename: "original_test.pdf".to_string(),
- file_path: "/path/to/test.pdf".to_string(),
- file_size: 1024,
- mime_type: "application/pdf".to_string(),
- source_type: Some("webdav".to_string()),
- source_path: Some("/webdav/test.pdf".to_string()),
- source_identifier: Some("webdav-server-1".to_string()),
- ignored_by: user.id,
- reason: Some("deleted by user".to_string()),
- };
-
- let created = create_ignored_file(&pool, ignored_file).await.unwrap();
-
- let result = delete_ignored_file(&pool, created.id, user.id).await;
- assert!(result.is_ok());
- assert!(result.unwrap());
-
- // Verify it's deleted
- let fetched = get_ignored_file_by_id(&pool, created.id, user.id).await;
- assert!(fetched.is_ok());
- assert!(fetched.unwrap().is_none());
- }
-
- #[tokio::test]
- async fn test_is_file_ignored() {
- let pool = create_test_db_pool().await;
- let user = create_test_user(&pool).await;
-
- let ignored_file = CreateIgnoredFile {
- file_hash: "test_hash".to_string(),
- filename: "test.pdf".to_string(),
- original_filename: "original_test.pdf".to_string(),
- file_path: "/path/to/test.pdf".to_string(),
- file_size: 1024,
- mime_type: "application/pdf".to_string(),
- source_type: Some("webdav".to_string()),
- source_path: Some("/webdav/test.pdf".to_string()),
- source_identifier: Some("webdav-server-1".to_string()),
- ignored_by: user.id,
- reason: Some("deleted by user".to_string()),
- };
-
- create_ignored_file(&pool, ignored_file).await.unwrap();
-
- // Test with exact match
- let result = is_file_ignored(
- &pool,
- "test_hash",
- Some("webdav"),
- Some("/webdav/test.pdf")
- ).await;
- assert!(result.is_ok());
- assert!(result.unwrap());
-
- // Test with just hash
- let result = is_file_ignored(&pool, "test_hash", None, None).await;
- assert!(result.is_ok());
- assert!(result.unwrap());
-
- // Test with non-existing hash
- let result = is_file_ignored(&pool, "non_existing", None, None).await;
- assert!(result.is_ok());
- assert!(!result.unwrap());
- }
-
- #[tokio::test]
- async fn test_create_ignored_file_from_document() {
- let pool = create_test_db_pool().await;
- let user = create_test_user(&pool).await;
- let document = create_test_document(&pool, user.id).await;
+ }).await.unwrap();
let result = create_ignored_file_from_document(
- &pool,
+ &ctx.state.db.pool,
document.id,
- user.id,
+ user.user_response.id,
Some("deleted by user".to_string()),
Some("webdav".to_string()),
Some("/webdav/test.pdf".to_string()),
@@ -315,7 +242,7 @@ mod tests {
assert_eq!(ignored_file.filename, document.filename);
assert_eq!(ignored_file.file_size, document.file_size);
assert_eq!(ignored_file.mime_type, document.mime_type);
- assert_eq!(ignored_file.ignored_by, user.id);
+ assert_eq!(ignored_file.ignored_by, user.user_response.id);
assert_eq!(ignored_file.source_type, Some("webdav".to_string()));
assert_eq!(ignored_file.reason, Some("deleted by user".to_string()));
}
diff --git a/src/tests/labels_tests.rs b/src/tests/labels_tests.rs
index a8ec2d2..497b4cb 100644
--- a/src/tests/labels_tests.rs
+++ b/src/tests/labels_tests.rs
@@ -3,79 +3,20 @@ mod tests {
use super::*;
use crate::models::UserRole;
use crate::routes::labels::{CreateLabel, UpdateLabel, LabelAssignment, Label};
+ use crate::test_utils::{TestContext, TestAuthHelper};
use axum::http::StatusCode;
use chrono::Utc;
use serde_json::json;
- use sqlx::{PgPool, Row};
+ use sqlx::Row;
use std::collections::HashMap;
- use testcontainers::{runners::AsyncRunner, ContainerAsync};
- use testcontainers_modules::postgres::Postgres;
use uuid::Uuid;
- struct TestContext {
- db: PgPool,
- _container: ContainerAsync,
- user_id: Uuid,
- admin_user_id: Uuid,
- }
-
- async fn setup_test_db() -> TestContext {
- // Start PostgreSQL container
- let postgres_image = Postgres::default();
- let container = postgres_image.start().await.expect("Failed to start postgres container");
- let port = container.get_host_port_ipv4(5432).await.expect("Failed to get postgres port");
-
- let connection_string = format!(
- "postgres://postgres:postgres@127.0.0.1:{}/postgres",
- port
- );
-
- // Connect to database
- let db = PgPool::connect(&connection_string)
- .await
- .expect("Failed to connect to test database");
-
- // Enable required extensions
- sqlx::query("CREATE EXTENSION IF NOT EXISTS pgcrypto;")
- .execute(&db)
- .await
- .expect("Failed to create pgcrypto extension");
-
- // Run migrations
- sqlx::migrate!("./migrations")
- .run(&db)
- .await
- .expect("Failed to run migrations");
-
- // Create test users
- let user_id = Uuid::new_v4();
- let admin_user_id = Uuid::new_v4();
-
- sqlx::query(
- r#"
- INSERT INTO users (id, username, email, password_hash, role, created_at, updated_at)
- VALUES
- ($1, 'testuser', 'test@example.com', 'hashed_password', 'user', NOW(), NOW()),
- ($2, 'admin', 'admin@example.com', 'hashed_password', 'admin', NOW(), NOW())
- "#,
- )
- .bind(user_id)
- .bind(admin_user_id)
- .execute(&db)
- .await
- .expect("Failed to create test users");
-
- TestContext {
- db,
- _container: container,
- user_id,
- admin_user_id,
- }
- }
#[tokio::test]
async fn test_create_label_success() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
let label_data = CreateLabel {
name: "Test Label".to_string(),
@@ -92,12 +33,12 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind(&label_data.name)
.bind(&label_data.description)
.bind(&label_data.color)
.bind(&label_data.icon)
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await;
assert!(result.is_ok());
@@ -108,7 +49,7 @@ mod tests {
"SELECT id, user_id, name, description, color, background_color, icon, is_system, created_at, updated_at, 0::bigint as document_count, 0::bigint as source_count FROM labels WHERE id = $1"
)
.bind(label_id)
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await
.expect("Failed to fetch created label");
@@ -116,13 +57,15 @@ mod tests {
assert_eq!(created_label.description.as_ref().unwrap(), "A test label");
assert_eq!(created_label.color, "#ff0000");
assert_eq!(created_label.icon.as_ref().unwrap(), "star");
- assert_eq!(created_label.user_id, Some(ctx.user_id));
+ assert_eq!(created_label.user_id, Some(user.user_response.id));
assert!(!created_label.is_system);
}
#[tokio::test]
async fn test_create_label_duplicate_name_fails() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create first label
sqlx::query(
@@ -131,10 +74,10 @@ mod tests {
VALUES ($1, $2, $3)
"#,
)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("Duplicate Name")
.bind("#ff0000")
- .execute(&ctx.db)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to create first label");
@@ -145,10 +88,10 @@ mod tests {
VALUES ($1, $2, $3)
"#,
)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("Duplicate Name")
.bind("#00ff00")
- .execute(&ctx.db)
+ .execute(&ctx.state.db.pool)
.await;
assert!(result.is_err());
@@ -157,7 +100,9 @@ mod tests {
#[tokio::test]
async fn test_update_label_success() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create label
let label_id = sqlx::query_scalar::<_, uuid::Uuid>(
@@ -167,10 +112,10 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("Original Name")
.bind("#ff0000")
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await
.unwrap();
@@ -201,8 +146,8 @@ mod tests {
.bind(&update_data.description)
.bind(&update_data.color)
.bind(&update_data.icon)
- .bind(ctx.user_id)
- .fetch_one(&ctx.db)
+ .bind(user.user_response.id)
+ .fetch_one(&ctx.state.db.pool)
.await;
assert!(result.is_ok());
@@ -216,7 +161,9 @@ mod tests {
#[tokio::test]
async fn test_delete_label_success() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create label
let label_id = sqlx::query_scalar::<_, uuid::Uuid>(
@@ -226,10 +173,10 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("To Delete")
.bind("#ff0000")
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await
.unwrap();
@@ -238,8 +185,8 @@ mod tests {
"DELETE FROM labels WHERE id = $1 AND user_id = $2 AND is_system = FALSE"
)
.bind(label_id)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await;
assert!(result.is_ok());
@@ -250,7 +197,7 @@ mod tests {
"SELECT id FROM labels WHERE id = $1"
)
.bind(label_id)
- .fetch_optional(&ctx.db)
+ .fetch_optional(&ctx.state.db.pool)
.await
.expect("Query failed");
@@ -259,7 +206,9 @@ mod tests {
#[tokio::test]
async fn test_cannot_delete_system_label() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create system label
let label_id = sqlx::query_scalar::<_, uuid::Uuid>(
@@ -273,7 +222,7 @@ mod tests {
.bind("System Label")
.bind("#ff0000")
.bind(true)
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await
.unwrap();
@@ -282,8 +231,8 @@ mod tests {
"DELETE FROM labels WHERE id = $1 AND user_id = $2 AND is_system = FALSE"
)
.bind(label_id)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await;
assert!(result.is_ok());
@@ -294,7 +243,7 @@ mod tests {
"SELECT id FROM labels WHERE id = $1"
)
.bind(label_id)
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await;
assert!(system_label.is_ok());
@@ -302,7 +251,9 @@ mod tests {
#[tokio::test]
async fn test_document_label_assignment() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create document
let document_id = Uuid::new_v4();
@@ -316,13 +267,13 @@ mod tests {
"#,
)
.bind(document_id)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("test.txt")
.bind("test.txt")
.bind("/test/test.txt")
.bind(1024)
.bind("text/plain")
- .execute(&ctx.db)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to create test document");
@@ -334,10 +285,10 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("Document Label")
.bind("#ff0000")
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await
.unwrap();
@@ -350,8 +301,8 @@ mod tests {
)
.bind(document_id)
.bind(label_id)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await;
assert!(result.is_ok());
@@ -367,7 +318,7 @@ mod tests {
)
.bind(document_id)
.bind(label_id)
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await;
assert!(assignment.is_ok());
@@ -375,12 +326,14 @@ mod tests {
let label_name: String = assignment.get("label_name");
let assigned_by: Option = assignment.get("assigned_by");
assert_eq!(label_name, "Document Label");
- assert_eq!(assigned_by.unwrap(), ctx.user_id);
+ assert_eq!(assigned_by.unwrap(), user.user_response.id);
}
#[tokio::test]
async fn test_document_label_removal() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create document and label
let document_id = Uuid::new_v4();
@@ -394,13 +347,13 @@ mod tests {
"#,
)
.bind(document_id)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("test.txt")
.bind("test.txt")
.bind("/test/test.txt")
.bind(1024)
.bind("text/plain")
- .execute(&ctx.db)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to create test document");
@@ -411,10 +364,10 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("Document Label")
.bind("#ff0000")
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await
.unwrap();
@@ -427,8 +380,8 @@ mod tests {
)
.bind(document_id)
.bind(label_id)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to assign label");
@@ -438,7 +391,7 @@ mod tests {
)
.bind(document_id)
.bind(label_id)
- .execute(&ctx.db)
+ .execute(&ctx.state.db.pool)
.await;
assert!(result.is_ok());
@@ -450,7 +403,7 @@ mod tests {
)
.bind(document_id)
.bind(label_id)
- .fetch_optional(&ctx.db)
+ .fetch_optional(&ctx.state.db.pool)
.await
.expect("Query failed");
@@ -459,7 +412,9 @@ mod tests {
#[tokio::test]
async fn test_get_document_labels() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create document
let document_id = Uuid::new_v4();
@@ -473,13 +428,13 @@ mod tests {
"#,
)
.bind(document_id)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind("test.txt")
.bind("test.txt")
.bind("/test/test.txt")
.bind(1024)
.bind("text/plain")
- .execute(&ctx.db)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to create test document");
@@ -493,10 +448,10 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind(name)
.bind(format!("#ff{:02x}00", i * 50))
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await
.unwrap();
label_ids.push(label_id);
@@ -512,8 +467,8 @@ mod tests {
)
.bind(document_id)
.bind(label_id)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to assign label");
}
@@ -529,7 +484,7 @@ mod tests {
"#,
)
.bind(document_id)
- .fetch_all(&ctx.db)
+ .fetch_all(&ctx.state.db.pool)
.await
.expect("Failed to fetch document labels");
@@ -544,7 +499,9 @@ mod tests {
#[tokio::test]
async fn test_label_usage_counts() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create label
let label_id = sqlx::query_scalar::<_, uuid::Uuid>(
@@ -554,8 +511,8 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
- .fetch_one(&ctx.db)
+ .bind(user.user_response.id)
+ .fetch_one(&ctx.state.db.pool)
.await
.unwrap();
@@ -573,10 +530,10 @@ mod tests {
"#,
)
.bind(doc_id)
- .bind(ctx.user_id)
+ .bind(user.user_response.id)
.bind(format!("test{}.txt", i))
.bind(format!("/test/test{}.txt", i))
- .execute(&ctx.db)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to create test document");
document_ids.push(doc_id);
@@ -592,8 +549,8 @@ mod tests {
)
.bind(doc_id)
.bind(label_id)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to assign label");
}
@@ -612,7 +569,7 @@ mod tests {
"#,
)
.bind(label_id)
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await
.expect("Failed to get usage count");
@@ -622,7 +579,9 @@ mod tests {
#[tokio::test]
async fn test_label_color_validation() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Test valid color
let valid_result = sqlx::query(
@@ -632,8 +591,8 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await;
assert!(valid_result.is_ok());
@@ -644,13 +603,13 @@ mod tests {
#[tokio::test]
async fn test_system_labels_migration() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
// Check that system labels were created by migration
let system_labels = sqlx::query(
"SELECT name FROM labels WHERE is_system = TRUE ORDER BY name"
)
- .fetch_all(&ctx.db)
+ .fetch_all(&ctx.state.db.pool)
.await
.expect("Failed to fetch system labels");
@@ -675,7 +634,9 @@ mod tests {
#[tokio::test]
async fn test_cascade_delete_on_document_removal() {
- let ctx = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
// Create document and label
let document_id = Uuid::new_v4();
@@ -689,8 +650,8 @@ mod tests {
"#,
)
.bind(document_id)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to create test document");
@@ -701,8 +662,8 @@ mod tests {
RETURNING id
"#,
)
- .bind(ctx.user_id)
- .fetch_one(&ctx.db)
+ .bind(user.user_response.id)
+ .fetch_one(&ctx.state.db.pool)
.await
.unwrap();
@@ -715,8 +676,8 @@ mod tests {
)
.bind(document_id)
.bind(label_id)
- .bind(ctx.user_id)
- .execute(&ctx.db)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to assign label");
@@ -725,7 +686,7 @@ mod tests {
"DELETE FROM documents WHERE id = $1"
)
.bind(document_id)
- .execute(&ctx.db)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to delete document");
@@ -734,7 +695,7 @@ mod tests {
"SELECT document_id FROM document_labels WHERE document_id = $1"
)
.bind(document_id)
- .fetch_all(&ctx.db)
+ .fetch_all(&ctx.state.db.pool)
.await
.expect("Query failed");
@@ -745,7 +706,7 @@ mod tests {
"SELECT id FROM labels WHERE id = $1"
)
.bind(label_id)
- .fetch_one(&ctx.db)
+ .fetch_one(&ctx.state.db.pool)
.await;
assert!(label.is_ok());
diff --git a/src/tests/settings_tests.rs b/src/tests/settings_tests.rs
index 9a928d9..be9a296 100644
--- a/src/tests/settings_tests.rs
+++ b/src/tests/settings_tests.rs
@@ -1,18 +1,19 @@
#[cfg(test)]
mod tests {
use crate::models::UpdateSettings;
- use crate::test_utils::{create_test_app, create_test_user, login_user};
+ use crate::test_utils::{TestContext, TestAuthHelper};
use axum::http::StatusCode;
use serde_json::json;
use tower::util::ServiceExt;
#[tokio::test]
async fn test_get_settings_default() {
- let (app, _container) = create_test_app().await;
- let user = create_test_user(&app).await;
- let token = login_user(&app, &user.username, "password123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
+ let token = auth_helper.login_user(&user.username, "password123").await;
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("GET")
@@ -40,9 +41,10 @@ mod tests {
#[tokio::test]
async fn test_update_settings() {
- let (app, _container) = create_test_app().await;
- let user = create_test_user(&app).await;
- let token = login_user(&app, &user.username, "password123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
+ let token = auth_helper.login_user(&user.username, "password123").await;
let update_data = UpdateSettings {
ocr_language: Some("spa".to_string()),
@@ -96,7 +98,7 @@ mod tests {
webdav_sync_interval_minutes: None,
};
- let response = app
+ let response = ctx.app
.clone()
.oneshot(
axum::http::Request::builder()
@@ -117,7 +119,7 @@ mod tests {
if status == StatusCode::OK {
// Verify the update
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("GET")
@@ -140,11 +142,12 @@ mod tests {
#[tokio::test]
async fn test_settings_isolated_per_user() {
- let (app, _container) = create_test_app().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
// Create two users
- let user1 = create_test_user(&app).await;
- let token1 = login_user(&app, &user1.username, "password123").await;
+ let user1 = auth_helper.create_test_user().await;
+ let token1 = auth_helper.login_user(&user1.username, "password123").await;
let user2_data = json!({
"username": "testuser2",
@@ -152,7 +155,7 @@ mod tests {
"password": "password456"
});
- let response = app
+ let response = ctx.app
.clone()
.oneshot(
axum::http::Request::builder()
@@ -166,7 +169,7 @@ mod tests {
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
- let token2 = login_user(&app, "testuser2", "password456").await;
+ let token2 = auth_helper.login_user("testuser2", "password456").await;
// Update user1's settings
let update_data = UpdateSettings {
@@ -221,7 +224,7 @@ mod tests {
webdav_sync_interval_minutes: None,
};
- let response = app
+ let response = ctx.app
.clone()
.oneshot(
axum::http::Request::builder()
@@ -242,7 +245,7 @@ mod tests {
if status == StatusCode::OK {
// Check user2's settings are still default
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("GET")
@@ -267,9 +270,9 @@ mod tests {
#[tokio::test]
async fn test_settings_requires_auth() {
- let (app, _container) = create_test_app().await;
+ let ctx = TestContext::new().await;
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("GET")
diff --git a/src/tests/unit_ocr_retry_db_tests_simple.rs b/src/tests/unit_ocr_retry_db_tests_simple.rs
index 769cbf4..0c9af51 100644
--- a/src/tests/unit_ocr_retry_db_tests_simple.rs
+++ b/src/tests/unit_ocr_retry_db_tests_simple.rs
@@ -1,53 +1,30 @@
#[cfg(test)]
mod tests {
use crate::db::ocr_retry::*;
- use sqlx::{PgPool, Row};
- use testcontainers::{runners::AsyncRunner, ContainerAsync};
- use testcontainers_modules::postgres::Postgres;
+ use crate::test_utils::{TestContext, TestAuthHelper};
+ use sqlx::Row;
use uuid::Uuid;
- async fn setup_test_db() -> (ContainerAsync, PgPool) {
- let postgres_image = Postgres::default();
- let container = postgres_image.start().await.expect("Failed to start postgres container");
- let port = container.get_host_port_ipv4(5432).await.expect("Failed to get postgres port");
-
- let connection_string = format!(
- "postgres://postgres:postgres@127.0.0.1:{}/postgres",
- port
- );
-
- let pool = PgPool::connect(&connection_string).await.expect("Failed to connect to test database");
- sqlx::migrate!("./migrations").run(&pool).await.expect("Failed to run migrations");
-
- (container, pool)
- }
-
#[tokio::test]
async fn test_simple_retry_record() {
- let (_container, pool) = setup_test_db().await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let user = auth_helper.create_test_user().await;
- // Create a simple test document entry first
+ // Create a test document using the TestContext database
let doc_id = Uuid::new_v4();
- let user_id = Uuid::new_v4();
-
- sqlx::query("INSERT INTO users (id, username, email, password_hash) VALUES ($1, 'test', 'test@test.com', 'test')")
- .bind(user_id)
- .execute(&pool)
- .await
- .expect("Failed to create test user");
-
- sqlx::query("INSERT INTO documents (id, filename, original_filename, user_id, mime_type, file_size, created_at, updated_at) VALUES ($1, 'test.pdf', 'test.pdf', $2, 'application/pdf', 1024, NOW(), NOW())")
+ sqlx::query("INSERT INTO documents (id, filename, original_filename, user_id, mime_type, file_size, created_at, updated_at, file_path) VALUES ($1, 'test.pdf', 'test.pdf', $2, 'application/pdf', 1024, NOW(), NOW(), '/test/test.pdf')")
.bind(doc_id)
- .bind(user_id)
- .execute(&pool)
+ .bind(user.user_response.id)
+ .execute(&ctx.state.db.pool)
.await
.expect("Failed to create test document");
// Test the record_ocr_retry function
let retry_id = record_ocr_retry(
- &pool,
+ &ctx.state.db.pool,
doc_id,
- user_id,
+ user.user_response.id,
"manual_retry",
10,
None,
@@ -56,7 +33,7 @@ mod tests {
// Verify the retry was recorded
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM ocr_retry_history WHERE id = $1")
.bind(retry_id)
- .fetch_one(&pool)
+ .fetch_one(&ctx.state.db.pool)
.await
.expect("Failed to count retries");
diff --git a/src/tests/users_tests.rs b/src/tests/users_tests.rs
index 3474075..b385537 100644
--- a/src/tests/users_tests.rs
+++ b/src/tests/users_tests.rs
@@ -1,7 +1,7 @@
#[cfg(test)]
mod tests {
use crate::models::{CreateUser, UpdateUser, UserResponse, AuthProvider, UserRole};
- use crate::test_utils::{create_test_app, create_test_user, create_admin_user, login_user};
+ use crate::test_utils::{TestContext, TestAuthHelper};
use axum::http::StatusCode;
use serde_json::json;
use tower::util::ServiceExt;
@@ -9,9 +9,10 @@ mod tests {
#[tokio::test]
async fn test_list_users() {
- let (app, _container) = create_test_app().await;
- let admin = create_admin_user(&app).await;
- let token = login_user(&app, &admin.username, "adminpass123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let admin = auth_helper.create_admin_user().await;
+ let token = auth_helper.login_user(&admin.username, "adminpass123").await;
// Create another user
let user2_data = json!({
@@ -20,7 +21,7 @@ mod tests {
"password": "password456"
});
- app.clone()
+ ctx.app.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
@@ -32,7 +33,7 @@ mod tests {
.await
.unwrap();
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("GET")
@@ -58,15 +59,16 @@ mod tests {
#[tokio::test]
async fn test_get_user_by_id() {
- let (app, _container) = create_test_app().await;
- let admin = create_admin_user(&app).await;
- let token = login_user(&app, &admin.username, "adminpass123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let admin = auth_helper.create_admin_user().await;
+ let token = auth_helper.login_user(&admin.username, "adminpass123").await;
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("GET")
- .uri(format!("/api/users/{}", admin.id))
+ .uri(format!("/api/users/{}", admin.id()))
.header("Authorization", format!("Bearer {}", token))
.body(axum::body::Body::empty())
.unwrap(),
@@ -81,16 +83,17 @@ mod tests {
.unwrap();
let fetched_user: UserResponse = serde_json::from_slice(&body).unwrap();
- assert_eq!(fetched_user.id, admin.id);
+ assert_eq!(fetched_user.id.to_string(), admin.id());
assert_eq!(fetched_user.username, admin.username);
- assert_eq!(fetched_user.email, admin.email);
+ assert_eq!(fetched_user.email, admin.user_response.email);
}
#[tokio::test]
async fn test_create_user_via_api() {
- let (app, _container) = create_test_app().await;
- let admin = create_admin_user(&app).await;
- let token = login_user(&app, &admin.username, "adminpass123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let admin = auth_helper.create_admin_user().await;
+ let token = auth_helper.login_user(&admin.username, "adminpass123").await;
let new_user_data = CreateUser {
username: "newuser".to_string(),
@@ -99,7 +102,7 @@ mod tests {
role: Some(crate::models::UserRole::User),
};
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("POST")
@@ -125,12 +128,13 @@ mod tests {
#[tokio::test]
async fn test_update_user() {
- let (app, _container) = create_test_app().await;
- let admin = create_admin_user(&app).await;
- let token = login_user(&app, &admin.username, "adminpass123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let admin = auth_helper.create_admin_user().await;
+ let token = auth_helper.login_user(&admin.username, "adminpass123").await;
// Create a regular user to update
- let user = create_test_user(&app).await;
+ let user = auth_helper.create_test_user().await;
let update_data = UpdateUser {
username: Some("updateduser".to_string()),
@@ -138,11 +142,11 @@ mod tests {
password: None,
};
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("PUT")
- .uri(format!("/api/users/{}", user.id))
+ .uri(format!("/api/users/{}", user.id()))
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&update_data).unwrap()))
@@ -164,12 +168,13 @@ mod tests {
#[tokio::test]
async fn test_update_user_password() {
- let (app, _container) = create_test_app().await;
- let admin = create_admin_user(&app).await;
- let token = login_user(&app, &admin.username, "adminpass123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let admin = auth_helper.create_admin_user().await;
+ let token = auth_helper.login_user(&admin.username, "adminpass123").await;
// Create a regular user to update
- let user = create_test_user(&app).await;
+ let user = auth_helper.create_test_user().await;
let update_data = UpdateUser {
username: None,
@@ -177,12 +182,12 @@ mod tests {
password: Some("newpassword456".to_string()),
};
- let response = app
+ let response = ctx.app
.clone()
.oneshot(
axum::http::Request::builder()
.method("PUT")
- .uri(format!("/api/users/{}", user.id))
+ .uri(format!("/api/users/{}", user.id()))
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&update_data).unwrap()))
@@ -194,15 +199,16 @@ mod tests {
assert_eq!(response.status(), StatusCode::OK);
// Verify new password works
- let new_token = login_user(&app, "testuser", "newpassword456").await;
+ let new_token = auth_helper.login_user("testuser", "newpassword456").await;
assert!(!new_token.is_empty());
}
#[tokio::test]
async fn test_delete_user() {
- let (app, _container) = create_test_app().await;
- let admin = create_admin_user(&app).await;
- let token = login_user(&app, &admin.username, "adminpass123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let admin = auth_helper.create_admin_user().await;
+ let token = auth_helper.login_user(&admin.username, "adminpass123").await;
// Create another user to delete
let user2_data = json!({
@@ -211,7 +217,7 @@ mod tests {
"password": "password456"
});
- let response = app
+ let response = ctx.app
.clone()
.oneshot(
axum::http::Request::builder()
@@ -230,7 +236,7 @@ mod tests {
let user2: UserResponse = serde_json::from_slice(&body).unwrap();
// Delete the user
- let response = app
+ let response = ctx.app
.clone()
.oneshot(
axum::http::Request::builder()
@@ -246,7 +252,7 @@ mod tests {
assert_eq!(response.status(), StatusCode::NO_CONTENT);
// Verify user is deleted
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("GET")
@@ -263,15 +269,16 @@ mod tests {
#[tokio::test]
async fn test_cannot_delete_self() {
- let (app, _container) = create_test_app().await;
- let admin = create_admin_user(&app).await;
- let token = login_user(&app, &admin.username, "adminpass123").await;
+ let ctx = TestContext::new().await;
+ let auth_helper = TestAuthHelper::new(ctx.app.clone());
+ let admin = auth_helper.create_admin_user().await;
+ let token = auth_helper.login_user(&admin.username, "adminpass123").await;
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("DELETE")
- .uri(format!("/api/users/{}", admin.id))
+ .uri(format!("/api/users/{}", admin.id()))
.header("Authorization", format!("Bearer {}", token))
.body(axum::body::Body::empty())
.unwrap(),
@@ -284,9 +291,9 @@ mod tests {
#[tokio::test]
async fn test_users_require_auth() {
- let (app, _container) = create_test_app().await;
+ let ctx = TestContext::new().await;
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("GET")
@@ -303,10 +310,8 @@ mod tests {
// OIDC Database Tests
#[tokio::test]
async fn test_create_oidc_user() {
- let (_app, container) = create_test_app().await;
- let port = container.get_host_port_ipv4(5432).await.unwrap();
- let database_url = format!("postgresql://test:test@localhost:{}/test", port);
- let db = crate::db::Database::new(&database_url).await.unwrap();
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
// Generate random identifiers to avoid test interference
let test_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
@@ -339,10 +344,8 @@ mod tests {
#[tokio::test]
async fn test_get_user_by_oidc_subject() {
- let (_app, container) = create_test_app().await;
- let port = container.get_host_port_ipv4(5432).await.unwrap();
- let database_url = format!("postgresql://test:test@localhost:{}/test", port);
- let db = crate::db::Database::new(&database_url).await.unwrap();
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
// Generate random identifiers to avoid test interference
let test_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
@@ -379,10 +382,8 @@ mod tests {
#[tokio::test]
async fn test_get_user_by_oidc_subject_not_found() {
- let (_app, container) = create_test_app().await;
- let port = container.get_host_port_ipv4(5432).await.unwrap();
- let database_url = format!("postgresql://test:test@localhost:{}/test", port);
- let db = crate::db::Database::new(&database_url).await.unwrap();
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
// Generate random subject that definitely doesn't exist
let test_id = uuid::Uuid::new_v4().to_string();
@@ -398,10 +399,8 @@ mod tests {
#[tokio::test]
async fn test_oidc_user_different_issuer() {
- let (_app, container) = create_test_app().await;
- let port = container.get_host_port_ipv4(5432).await.unwrap();
- let database_url = format!("postgresql://test:test@localhost:{}/test", port);
- let db = crate::db::Database::new(&database_url).await.unwrap();
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
// Generate random identifiers to avoid test interference
let test_id = uuid::Uuid::new_v4().to_string()[..8].to_string();
@@ -435,10 +434,8 @@ mod tests {
#[tokio::test]
async fn test_local_user_login_works() {
- let (app, container) = create_test_app().await;
- let port = container.get_host_port_ipv4(5432).await.unwrap();
- let database_url = format!("postgresql://test:test@localhost:{}/test", port);
- let db = crate::db::Database::new(&database_url).await.unwrap();
+ let ctx = TestContext::new().await;
+ let db = &ctx.state.db;
// Create regular local user
let create_user = CreateUser {
@@ -460,7 +457,7 @@ mod tests {
"password": "password123"
});
- let response = app
+ let response = ctx.app
.oneshot(
axum::http::Request::builder()
.method("POST")