feat(tests): try to deduplicate test code even more

This commit is contained in:
perf3ct 2025-07-03 19:17:33 +00:00
parent 7993786e18
commit a3f49f9bd7
21 changed files with 618 additions and 1116 deletions

View File

@ -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();
});

View File

@ -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()) {

View File

@ -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'

View File

@ -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 }) => (
<AuthProvider>
{children}
</AuthProvider>
);
const MockThemeProvider = ({ children }: { children: React.ReactNode }) => (
<ThemeProvider>
{children}
</ThemeProvider>
);
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(
<BrowserRouter>
<MockThemeProvider>
<MockAuthProvider>
<Login />
</MockAuthProvider>
</MockThemeProvider>
</BrowserRouter>
);
return renderWithProviders(<Login />);
};
it('renders OIDC login button', () => {

View File

@ -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 }) => (
<AuthProvider>
{children}
</AuthProvider>
);
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(
<MemoryRouter initialEntries={[`/auth/oidc/callback${search}`]}>
<MockAuthProvider>
<Routes>
<Route path="/auth/oidc/callback" element={<OidcCallback />} />
<Route path="/login" element={<div>Login Page</div>} />
</Routes>
</MockAuthProvider>
<Routes>
<Route path="/auth/oidc/callback" element={<OidcCallback />} />
<Route path="/login" element={<div>Login Page</div>} />
</Routes>
</MemoryRouter>
);
};

View File

@ -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);
});

View File

@ -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<React.ComponentProps<typeof Label>> = {}) => {
const defaultProps = {
@ -34,14 +26,14 @@ const renderLabel = (props: Partial<React.ComponentProps<typeof Label>> = {}) =>
...props,
};
return render(
<ThemeProvider theme={theme}>
<Label {...defaultProps} />
</ThemeProvider>
);
return renderWithProviders(<Label {...defaultProps} />);
};
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', () => {

View File

@ -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<React.ComponentProps<typeof LabelCreateDialog>> = {}) => {
const defaultProps = {
@ -29,17 +26,14 @@ const renderLabelCreateDialog = (props: Partial<React.ComponentProps<typeof Labe
...props,
};
return render(
<ThemeProvider theme={theme}>
<LabelCreateDialog {...defaultProps} />
</ThemeProvider>
);
return renderWithProviders(<LabelCreateDialog {...defaultProps} />);
};
describe('LabelCreateDialog Component', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
setupTestEnvironment();
user = userEvent.setup();
});

View File

@ -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<React.ComponentProps<typeof LabelSelector>> = {}) => {
const defaultProps = {
@ -54,17 +19,14 @@ const renderLabelSelector = (props: Partial<React.ComponentProps<typeof LabelSel
...props,
};
return render(
<ThemeProvider theme={theme}>
<LabelSelector {...defaultProps} />
</ThemeProvider>
);
return renderWithProviders(<LabelSelector {...defaultProps} />);
};
describe('LabelSelector Component', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
setupTestEnvironment();
user = userEvent.setup();
});

View File

@ -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(
<NotificationProvider>
{component}
</NotificationProvider>
);
});
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(<UploadZone {...mockProps} />);
await act(async () => {
renderWithProviders(<UploadZone {...mockProps} />);
});
// Wait for async operations to complete
await waitFor(() => {

View File

@ -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(
<BrowserRouter>
<ThemeProvider theme={theme}>
<LabelsPage />
</ThemeProvider>
</BrowserRouter>
);
renderResult = renderWithAuthenticatedUser(<LabelsPage />);
});
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 });

View File

@ -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 <BrowserRouter>{children}</BrowserRouter>;
};
describe('SearchPage', () => {
beforeEach(() => {
vi.clearAllMocks();
setupTestEnvironment();
});
test('renders search page structure', () => {
render(
<SearchPageWrapper>
<SearchPage />
</SearchPageWrapper>
);
renderWithAuthenticatedUser(<SearchPage />);
// Check for page title
expect(screen.getByText('Search Documents')).toBeInTheDocument();
@ -38,11 +31,7 @@ describe('SearchPage', () => {
});
test('renders search input', () => {
render(
<SearchPageWrapper>
<SearchPage />
</SearchPageWrapper>
);
renderWithAuthenticatedUser(<SearchPage />);
const searchInput = screen.getByPlaceholderText(/search/i);
expect(searchInput).toBeInTheDocument();
@ -142,11 +131,7 @@ describe('SearchPage', () => {
// });
test('renders main search container', () => {
const { container } = render(
<SearchPageWrapper>
<SearchPage />
</SearchPageWrapper>
);
const { container } = renderWithAuthenticatedUser(<SearchPage />);
expect(container.firstChild).toBeInTheDocument();
});

View File

@ -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');

View File

@ -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();

View File

@ -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");

View File

@ -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();

View File

@ -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()));
}

View File

@ -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<Postgres>,
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<uuid::Uuid> = 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());

View File

@ -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")

View File

@ -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<Postgres>, 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");

View File

@ -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")