feat(tests): deduplicate tests too

This commit is contained in:
perf3ct 2025-07-03 17:21:39 +00:00
parent 84ebcc9830
commit 7993786e18
11 changed files with 1061 additions and 422 deletions

View File

@ -1,53 +1,124 @@
import { test as base, expect } from '@playwright/test'; import { test as base, expect } from '@playwright/test';
import type { Page } from '@playwright/test'; import type { Page } from '@playwright/test';
// Centralized test credentials to eliminate duplication
export const TEST_CREDENTIALS = {
admin: {
username: 'admin',
password: 'readur2024'
},
user: {
username: 'user',
password: 'userpass123'
}
} as const;
export const TIMEOUTS = {
login: 10000,
navigation: 10000,
api: 5000
} as const;
export interface AuthFixture { export interface AuthFixture {
authenticatedPage: Page; authenticatedPage: Page;
adminPage: Page;
userPage: Page;
}
// Shared authentication helper functions
export class AuthHelper {
constructor(private page: Page) {}
async loginAs(credentials: typeof TEST_CREDENTIALS.admin | typeof TEST_CREDENTIALS.user) {
console.log(`Attempting to login as ${credentials.username}...`);
// Go to home page
await this.page.goto('/');
await this.page.waitForLoadState('networkidle');
// Check if already logged in
const usernameInput = await this.page.locator('input[name="username"]').isVisible().catch(() => false);
if (!usernameInput) {
console.log('Already logged in or no login form found');
return;
}
// Fill login form
await this.page.fill('input[name="username"]', credentials.username);
await this.page.fill('input[name="password"]', credentials.password);
// Wait for login API response
const loginPromise = this.page.waitForResponse(response =>
response.url().includes('/auth/login') && response.status() === 200,
{ timeout: TIMEOUTS.login }
);
await this.page.click('button[type="submit"]');
try {
await loginPromise;
console.log(`Login as ${credentials.username} successful`);
// Wait for navigation away from login page
await this.page.waitForFunction(() =>
!window.location.pathname.includes('/login'),
{ timeout: TIMEOUTS.navigation }
);
console.log('Navigation completed to:', this.page.url());
} catch (error) {
console.error(`Login as ${credentials.username} failed:`, error);
throw error;
}
}
async logout() {
// Look for logout button/link and click it
const logoutButton = this.page.locator('[data-testid="logout"], button:has-text("Logout"), a:has-text("Logout")').first();
if (await logoutButton.isVisible()) {
await logoutButton.click();
// Wait for redirect to login page
await this.page.waitForFunction(() =>
window.location.pathname.includes('/login') || window.location.pathname === '/',
{ timeout: TIMEOUTS.navigation }
);
}
}
async ensureLoggedOut() {
await this.page.goto('/');
await this.page.waitForLoadState('networkidle');
// If we see a login form, we're already logged out
const usernameInput = await this.page.locator('input[name="username"]').isVisible().catch(() => false);
if (usernameInput) {
return;
}
// Otherwise, try to logout
await this.logout();
}
} }
export const test = base.extend<AuthFixture>({ export const test = base.extend<AuthFixture>({
authenticatedPage: async ({ page }, use) => { authenticatedPage: async ({ page }, use) => {
await page.goto('/'); const auth = new AuthHelper(page);
await auth.loginAs(TEST_CREDENTIALS.admin);
// Wait a bit for the page to load await use(page);
await page.waitForLoadState('networkidle'); },
// Check if already logged in by looking for username input (login page) adminPage: async ({ page }, use) => {
const usernameInput = await page.locator('input[name="username"]').isVisible().catch(() => false); const auth = new AuthHelper(page);
await auth.loginAs(TEST_CREDENTIALS.admin);
if (usernameInput) { await use(page);
console.log('Found login form, attempting to login...'); },
// Fill login form with demo credentials userPage: async ({ page }, use) => {
await page.fill('input[name="username"]', 'admin'); const auth = new AuthHelper(page);
await page.fill('input[name="password"]', 'readur2024'); await auth.loginAs(TEST_CREDENTIALS.user);
// Wait for the login API call response
const loginPromise = page.waitForResponse(response =>
response.url().includes('/auth/login') && response.status() === 200,
{ timeout: 10000 }
);
await page.click('button[type="submit"]');
try {
await loginPromise;
console.log('Login API call successful');
// Wait for redirect or URL change
await page.waitForFunction(() =>
!window.location.pathname.includes('/login'),
{ timeout: 10000 }
);
console.log('Redirected to:', page.url());
} catch (error) {
console.log('Login failed or timeout:', error);
}
} else {
console.log('Already logged in or no login form found');
}
await use(page); await use(page);
}, },
}); });

View File

@ -0,0 +1,218 @@
import { test, expect } from './fixtures/auth';
import { TIMEOUTS, API_ENDPOINTS } from './utils/test-data';
import { TestHelpers } from './utils/test-helpers';
test.describe('OCR Retry Workflow', () => {
let helpers: TestHelpers;
test.beforeEach(async ({ adminPage }) => {
helpers = new TestHelpers(adminPage);
await helpers.navigateToPage('/documents');
});
test('should display failed OCR documents', async ({ adminPage: page }) => {
await page.goto('/documents');
await helpers.waitForLoadingToComplete();
// Look for failed documents filter or section
const failedFilter = page.locator('button:has-text("Failed"), [data-testid="failed-filter"], .filter-failed').first();
if (await failedFilter.isVisible()) {
await failedFilter.click();
await helpers.waitForLoadingToComplete();
} else {
// Alternative: look for a dedicated failed documents page
const failedTab = page.locator('tab:has-text("Failed"), [role="tab"]:has-text("Failed")').first();
if (await failedTab.isVisible()) {
await failedTab.click();
await helpers.waitForLoadingToComplete();
}
}
// Check if failed documents are displayed
const documentList = page.locator('[data-testid="document-list"], .document-list, .documents-grid');
if (await documentList.isVisible({ timeout: 5000 })) {
const documents = page.locator('.document-item, .document-card, [data-testid="document-item"]');
const documentCount = await documents.count();
console.log(`Found ${documentCount} documents in failed OCR view`);
}
});
test('should retry individual failed OCR document', async ({ adminPage: page }) => {
await page.goto('/documents');
await helpers.waitForLoadingToComplete();
// Navigate to failed documents
const failedFilter = page.locator('button:has-text("Failed"), [data-testid="failed-filter"]').first();
if (await failedFilter.isVisible()) {
await failedFilter.click();
await helpers.waitForLoadingToComplete();
}
// Find a failed document and its retry button
const retryButton = page.locator('button:has-text("Retry"), [data-testid="retry-ocr"], .retry-button').first();
if (await retryButton.isVisible()) {
// Wait for retry API call
const retryPromise = page.waitForResponse(response =>
response.url().includes('/retry') && response.status() === 200,
{ timeout: TIMEOUTS.medium }
);
await retryButton.click();
try {
await retryPromise;
console.log('OCR retry initiated successfully');
// Look for success message or status change
const successMessage = page.locator('.success, [data-testid="success-message"], .notification');
if (await successMessage.isVisible({ timeout: 5000 })) {
console.log('Retry success message displayed');
}
} catch (error) {
console.log('OCR retry may have failed:', error);
}
}
});
test('should bulk retry multiple failed OCR documents', async ({ adminPage: page }) => {
await page.goto('/documents');
await helpers.waitForLoadingToComplete();
// Navigate to failed documents
const failedFilter = page.locator('button:has-text("Failed"), [data-testid="failed-filter"]').first();
if (await failedFilter.isVisible()) {
await failedFilter.click();
await helpers.waitForLoadingToComplete();
}
// Select multiple documents
const selectAllCheckbox = page.locator('input[type="checkbox"]:has-text("Select All"), [data-testid="select-all"]').first();
if (await selectAllCheckbox.isVisible()) {
await selectAllCheckbox.click();
} else {
// Alternative: select individual checkboxes
const documentCheckboxes = page.locator('.document-item input[type="checkbox"], [data-testid="document-checkbox"]');
const checkboxCount = await documentCheckboxes.count();
if (checkboxCount > 0) {
// Select first 3 documents
for (let i = 0; i < Math.min(3, checkboxCount); i++) {
await documentCheckboxes.nth(i).click();
}
}
}
// Find bulk retry button
const bulkRetryButton = page.locator('button:has-text("Retry Selected"), button:has-text("Bulk Retry"), [data-testid="bulk-retry"]').first();
if (await bulkRetryButton.isVisible()) {
// Wait for bulk retry API call
const bulkRetryPromise = page.waitForResponse(response =>
response.url().includes('/bulk-retry') || response.url().includes('/retry'),
{ timeout: TIMEOUTS.long }
);
await bulkRetryButton.click();
try {
await bulkRetryPromise;
console.log('Bulk OCR retry initiated successfully');
// Look for progress indicator or success message
const progressIndicator = page.locator('.progress, [data-testid="retry-progress"], .bulk-retry-progress');
if (await progressIndicator.isVisible({ timeout: 5000 })) {
console.log('Bulk retry progress indicator visible');
}
} catch (error) {
console.log('Bulk OCR retry may have failed:', error);
}
}
});
test('should show OCR retry history', async ({ adminPage: page }) => {
await page.goto('/documents');
await helpers.waitForLoadingToComplete();
// Look for retry history or logs
const historyButton = page.locator('button:has-text("Retry History"), [data-testid="retry-history"], .history-button').first();
if (await historyButton.isVisible()) {
await historyButton.click();
// Check if history modal or panel opens
const historyContainer = page.locator('.retry-history, [data-testid="retry-history-panel"], .history-container');
await expect(historyContainer.first()).toBeVisible({ timeout: TIMEOUTS.short });
// Check for history entries
const historyEntries = page.locator('.history-item, .retry-entry, tr');
if (await historyEntries.first().isVisible({ timeout: 5000 })) {
const entryCount = await historyEntries.count();
console.log(`Found ${entryCount} retry history entries`);
}
}
});
test('should display OCR failure reasons', async ({ adminPage: page }) => {
await page.goto('/documents');
await helpers.waitForLoadingToComplete();
// Navigate to failed documents
const failedFilter = page.locator('button:has-text("Failed"), [data-testid="failed-filter"]').first();
if (await failedFilter.isVisible()) {
await failedFilter.click();
await helpers.waitForLoadingToComplete();
}
// Click on a failed document to view details
const failedDocument = page.locator('.document-item, .document-card, [data-testid="document-item"]').first();
if (await failedDocument.isVisible()) {
await failedDocument.click();
// Look for failure reason or error details
const errorDetails = page.locator('.error-details, [data-testid="failure-reason"], .ocr-error');
if (await errorDetails.isVisible({ timeout: 5000 })) {
const errorText = await errorDetails.textContent();
console.log('OCR failure reason:', errorText);
}
// Look for retry recommendations
const recommendations = page.locator('.retry-recommendations, [data-testid="retry-suggestions"], .recommendations');
if (await recommendations.isVisible({ timeout: 5000 })) {
console.log('Retry recommendations displayed');
}
}
});
test('should filter failed documents by failure type', async ({ adminPage: page }) => {
await page.goto('/documents');
await helpers.waitForLoadingToComplete();
// Navigate to failed documents
const failedFilter = page.locator('button:has-text("Failed"), [data-testid="failed-filter"]').first();
if (await failedFilter.isVisible()) {
await failedFilter.click();
await helpers.waitForLoadingToComplete();
}
// Look for failure type filters
const filterDropdown = page.locator('select[name="failure-type"], [data-testid="failure-filter"]').first();
if (await filterDropdown.isVisible()) {
await filterDropdown.click();
// Select a specific failure type
const timeoutOption = page.locator('option:has-text("Timeout"), [value="timeout"]').first();
if (await timeoutOption.isVisible()) {
await timeoutOption.click();
await helpers.waitForLoadingToComplete();
// Verify filtered results
const filteredDocuments = page.locator('.document-item, .document-card');
const documentCount = await filteredDocuments.count();
console.log(`Found ${documentCount} documents with timeout failures`);
}
}
});
});

View File

@ -0,0 +1,203 @@
import { test, expect } from './fixtures/auth';
import { TIMEOUTS, API_ENDPOINTS } from './utils/test-data';
import { TestHelpers } from './utils/test-helpers';
test.describe('WebDAV Workflow', () => {
let helpers: TestHelpers;
test.beforeEach(async ({ adminPage }) => {
helpers = new TestHelpers(adminPage);
await helpers.navigateToPage('/sources');
});
test('should create and configure WebDAV source', async ({ adminPage: page }) => {
// Navigate to sources page
await page.goto('/sources');
await helpers.waitForLoadingToComplete();
// Look for add source button (try multiple selectors)
const addSourceButton = page.locator('button:has-text("Add"), button:has-text("New"), [data-testid="add-source"]').first();
if (await addSourceButton.isVisible()) {
await addSourceButton.click();
} else {
// Alternative: look for floating action button or plus button
const fabButton = page.locator('button[aria-label*="add"], button[title*="add"], .fab, .add-button').first();
if (await fabButton.isVisible()) {
await fabButton.click();
}
}
// Wait for source creation form/modal
await page.waitForTimeout(1000);
// Select WebDAV source type if source type selection exists
const webdavOption = page.locator('input[value="webdav"], [data-value="webdav"], option[value="webdav"]').first();
if (await webdavOption.isVisible()) {
await webdavOption.click();
}
// Fill WebDAV configuration form
const nameInput = page.locator('input[name="name"], input[placeholder*="name"], input[label*="Name"]').first();
if (await nameInput.isVisible()) {
await nameInput.fill('Test WebDAV Source');
}
const urlInput = page.locator('input[name="url"], input[placeholder*="url"], input[type="url"]').first();
if (await urlInput.isVisible()) {
await urlInput.fill('https://demo.webdav.server/');
}
const usernameInput = page.locator('input[name="username"], input[placeholder*="username"]').first();
if (await usernameInput.isVisible()) {
await usernameInput.fill('webdav_user');
}
const passwordInput = page.locator('input[name="password"], input[type="password"]').first();
if (await passwordInput.isVisible()) {
await passwordInput.fill('webdav_pass');
}
// Save the source configuration
const saveButton = page.locator('button:has-text("Save"), button:has-text("Create"), button[type="submit"]').first();
if (await saveButton.isVisible()) {
// Wait for save API call
const savePromise = page.waitForResponse(response =>
response.url().includes('/sources') && (response.status() === 200 || response.status() === 201),
{ timeout: TIMEOUTS.medium }
);
await saveButton.click();
try {
await savePromise;
console.log('WebDAV source created successfully');
} catch (error) {
console.log('Source creation may have failed or timed out:', error);
}
}
// Verify source appears in the list
await helpers.waitForLoadingToComplete();
const sourceList = page.locator('[data-testid="sources-list"], .sources-list, .source-item');
await expect(sourceList.first()).toBeVisible({ timeout: TIMEOUTS.medium });
});
test('should test WebDAV connection', async ({ adminPage: page }) => {
// This test assumes a WebDAV source exists from the previous test or setup
await page.goto('/sources');
await helpers.waitForLoadingToComplete();
// Find WebDAV source and test connection
const testConnectionButton = page.locator('button:has-text("Test"), [data-testid="test-connection"]').first();
if (await testConnectionButton.isVisible()) {
// Wait for connection test API call
const testPromise = page.waitForResponse(response =>
response.url().includes('/test') || response.url().includes('/connection'),
{ timeout: TIMEOUTS.medium }
);
await testConnectionButton.click();
try {
const response = await testPromise;
console.log('Connection test completed with status:', response.status());
} catch (error) {
console.log('Connection test may have failed:', error);
}
}
// Look for connection status indicator
const statusIndicator = page.locator('.status, [data-testid="connection-status"], .connection-result');
if (await statusIndicator.isVisible()) {
const statusText = await statusIndicator.textContent();
console.log('Connection status:', statusText);
}
});
test('should initiate WebDAV sync', async ({ adminPage: page }) => {
await page.goto('/sources');
await helpers.waitForLoadingToComplete();
// Find and click sync button
const syncButton = page.locator('button:has-text("Sync"), [data-testid="sync-source"]').first();
if (await syncButton.isVisible()) {
// Wait for sync API call
const syncPromise = page.waitForResponse(response =>
response.url().includes('/sync') && response.status() === 200,
{ timeout: TIMEOUTS.medium }
);
await syncButton.click();
try {
await syncPromise;
console.log('WebDAV sync initiated successfully');
// Look for sync progress indicators
const progressIndicator = page.locator('.progress, [data-testid="sync-progress"], .syncing');
if (await progressIndicator.isVisible({ timeout: 5000 })) {
console.log('Sync progress indicator visible');
}
} catch (error) {
console.log('Sync may have failed or timed out:', error);
}
}
});
test('should show WebDAV sync history', async ({ adminPage: page }) => {
await page.goto('/sources');
await helpers.waitForLoadingToComplete();
// Look for sync history or logs
const historyButton = page.locator('button:has-text("History"), button:has-text("Logs"), [data-testid="sync-history"]').first();
if (await historyButton.isVisible()) {
await historyButton.click();
// Check if history modal or page opens
const historyContainer = page.locator('.history, [data-testid="sync-history"], .logs-container');
await expect(historyContainer.first()).toBeVisible({ timeout: TIMEOUTS.short });
// Check for history entries
const historyEntries = page.locator('.history-item, .log-entry, tr');
if (await historyEntries.first().isVisible({ timeout: 5000 })) {
const entryCount = await historyEntries.count();
console.log(`Found ${entryCount} sync history entries`);
}
}
});
test('should handle WebDAV source deletion', async ({ adminPage: page }) => {
await page.goto('/sources');
await helpers.waitForLoadingToComplete();
// Find delete button for WebDAV source
const deleteButton = page.locator('button:has-text("Delete"), [data-testid="delete-source"], .delete-button').first();
if (await deleteButton.isVisible()) {
await deleteButton.click();
// Handle confirmation dialog if it appears
const confirmButton = page.locator('button:has-text("Confirm"), button:has-text("Delete"), button:has-text("Yes")').first();
if (await confirmButton.isVisible({ timeout: 3000 })) {
// Wait for delete API call
const deletePromise = page.waitForResponse(response =>
response.url().includes('/sources') && response.status() === 200,
{ timeout: TIMEOUTS.medium }
);
await confirmButton.click();
try {
await deletePromise;
console.log('WebDAV source deleted successfully');
} catch (error) {
console.log('Source deletion may have failed:', error);
}
}
}
});
});

View File

@ -1,18 +1,12 @@
import React from 'react'; import React from 'react';
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 userEvent from '@testing-library/user-event';
import { BrowserRouter } from 'react-router-dom';
import { vi } from 'vitest'; import { vi } from 'vitest';
import GlobalSearchBar from '../GlobalSearchBar'; import GlobalSearchBar from '../GlobalSearchBar';
import { renderWithProviders, createMockApiServices } from '../../../test/test-utils';
// Mock the API service // Use centralized API mocking
const mockDocumentService = { const mockServices = createMockApiServices();
enhancedSearch: vi.fn(),
};
vi.mock('../../../services/api', () => ({
documentService: mockDocumentService,
}));
// Mock useNavigate // Mock useNavigate
const mockNavigate = vi.fn(); const mockNavigate = vi.fn();
@ -24,15 +18,6 @@ vi.mock('react-router-dom', async () => {
}; };
}); });
// Mock localStorage
const localStorageMock = {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
clear: vi.fn(),
};
global.localStorage = localStorageMock;
// Mock data // Mock data
const mockSearchResponse = { const mockSearchResponse = {
data: { data: {
@ -53,14 +38,7 @@ const mockSearchResponse = {
} }
}; };
// Helper to render component with router // Using centralized render utility (no custom helper needed)
const renderWithRouter = (component) => {
return render(
<BrowserRouter>
{component}
</BrowserRouter>
);
};
describe('GlobalSearchBar', () => { describe('GlobalSearchBar', () => {
beforeEach(() => { beforeEach(() => {
@ -70,7 +48,7 @@ describe('GlobalSearchBar', () => {
}); });
test('renders search input with placeholder', () => { test('renders search input with placeholder', () => {
renderWithRouter(<GlobalSearchBar />); renderWithProviders(<GlobalSearchBar />);
expect(screen.getByPlaceholderText('Search documents...')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Search documents...')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toBeInTheDocument(); expect(screen.getByRole('textbox')).toBeInTheDocument();
@ -78,7 +56,7 @@ describe('GlobalSearchBar', () => {
test('accepts user input', async () => { test('accepts user input', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderWithRouter(<GlobalSearchBar />); renderWithProviders(<GlobalSearchBar />);
const searchInput = screen.getByPlaceholderText('Search documents...'); const searchInput = screen.getByPlaceholderText('Search documents...');
await user.type(searchInput, 'test'); await user.type(searchInput, 'test');
@ -88,7 +66,7 @@ describe('GlobalSearchBar', () => {
test('clears input when clear button is clicked', async () => { test('clears input when clear button is clicked', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderWithRouter(<GlobalSearchBar />); renderWithProviders(<GlobalSearchBar />);
const searchInput = screen.getByPlaceholderText('Search documents...'); const searchInput = screen.getByPlaceholderText('Search documents...');
await user.type(searchInput, 'test'); await user.type(searchInput, 'test');
@ -101,7 +79,7 @@ describe('GlobalSearchBar', () => {
}); });
test('shows popular searches when focused', async () => { test('shows popular searches when focused', async () => {
renderWithRouter(<GlobalSearchBar />); renderWithProviders(<GlobalSearchBar />);
const searchInput = screen.getByPlaceholderText('Search documents...'); const searchInput = screen.getByPlaceholderText('Search documents...');
fireEvent.focus(searchInput); fireEvent.focus(searchInput);
@ -112,7 +90,7 @@ describe('GlobalSearchBar', () => {
}); });
test('handles empty search gracefully', () => { test('handles empty search gracefully', () => {
renderWithRouter(<GlobalSearchBar />); renderWithProviders(<GlobalSearchBar />);
const searchInput = screen.getByPlaceholderText('Search documents...'); const searchInput = screen.getByPlaceholderText('Search documents...');
fireEvent.change(searchInput, { target: { value: '' } }); fireEvent.change(searchInput, { target: { value: '' } });
@ -122,7 +100,7 @@ describe('GlobalSearchBar', () => {
test('handles keyboard navigation', async () => { test('handles keyboard navigation', async () => {
const user = userEvent.setup(); const user = userEvent.setup();
renderWithRouter(<GlobalSearchBar />); renderWithProviders(<GlobalSearchBar />);
const searchInput = screen.getByPlaceholderText('Search documents...'); const searchInput = screen.getByPlaceholderText('Search documents...');
await user.type(searchInput, 'test query'); await user.type(searchInput, 'test query');

View File

@ -1,37 +1,26 @@
import { expect, afterEach, vi } from 'vitest' // Global test setup file for Vitest
import { cleanup } from '@testing-library/react' // This file is automatically loaded before all tests
import * as matchers from '@testing-library/jest-dom/matchers'
expect.extend(matchers) import '@testing-library/jest-dom'
import { vi } from 'vitest'
import { setupTestEnvironment } from './test-utils'
afterEach(() => { // Setup global test environment
cleanup() setupTestEnvironment()
// Additional global setup can be added here
// For example:
// - Global error handlers
// - Test timeouts
// - Common test data
// - Global test utilities
// Increase test timeout for async operations
beforeEach(() => {
vi.resetAllMocks()
}) })
// Global axios mock // Clean up after each test
vi.mock('axios', () => ({ afterEach(() => {
default: { vi.clearAllMocks()
create: vi.fn(() => ({
get: vi.fn(() => Promise.resolve({ data: [] })),
post: vi.fn(() => Promise.resolve({ data: {} })),
put: vi.fn(() => Promise.resolve({ data: {} })),
delete: vi.fn(() => Promise.resolve({ data: {} })),
defaults: { headers: { common: {} } },
})),
},
}))
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
}) })

View File

@ -7,6 +7,7 @@ interface User {
id: string id: string
username: string username: string
email: string email: string
role?: string
} }
interface MockAuthContextType { interface MockAuthContextType {
@ -17,6 +18,85 @@ interface MockAuthContextType {
logout: () => void logout: () => void
} }
// Test data factories for consistent mock data across tests
export const createMockUser = (overrides: Partial<User> = {}): User => ({
id: '1',
username: 'testuser',
email: 'test@example.com',
role: 'user',
...overrides
})
export const createMockAdminUser = (overrides: Partial<User> = {}): User => ({
id: '2',
username: 'adminuser',
email: 'admin@example.com',
role: 'admin',
...overrides
})
// Centralized API mocking to eliminate per-file duplication
export const createMockApiServices = () => {
const mockDocumentService = {
enhancedSearch: vi.fn().mockResolvedValue({ documents: [], total: 0 }),
bulkRetryOcr: vi.fn().mockResolvedValue({ success: true }),
getDocument: vi.fn().mockResolvedValue({}),
uploadDocument: vi.fn().mockResolvedValue({}),
deleteDocument: vi.fn().mockResolvedValue({}),
updateDocument: vi.fn().mockResolvedValue({}),
}
const mockAuthService = {
login: vi.fn().mockResolvedValue({ token: 'mock-token', user: createMockUser() }),
register: vi.fn().mockResolvedValue({ token: 'mock-token', user: createMockUser() }),
logout: vi.fn().mockResolvedValue({}),
getCurrentUser: vi.fn().mockResolvedValue(createMockUser()),
}
const mockSourceService = {
getSources: vi.fn().mockResolvedValue([]),
createSource: vi.fn().mockResolvedValue({}),
updateSource: vi.fn().mockResolvedValue({}),
deleteSource: vi.fn().mockResolvedValue({}),
syncSource: vi.fn().mockResolvedValue({}),
}
const mockLabelService = {
getLabels: vi.fn().mockResolvedValue([]),
createLabel: vi.fn().mockResolvedValue({}),
updateLabel: vi.fn().mockResolvedValue({}),
deleteLabel: vi.fn().mockResolvedValue({}),
}
return {
documentService: mockDocumentService,
authService: mockAuthService,
sourceService: mockSourceService,
labelService: mockLabelService,
}
}
// Setup global API mocks (call this in setup files)
export const setupApiMocks = () => {
const mockServices = createMockApiServices()
vi.mock('../../services/api', () => ({
documentService: mockServices.documentService,
authService: mockServices.authService,
sourceService: mockServices.sourceService,
labelService: mockServices.labelService,
api: {
defaults: {
headers: {
common: {}
}
}
}
}))
return mockServices
}
// Create a mock AuthProvider for testing // Create a mock AuthProvider for testing
export const MockAuthProvider = ({ export const MockAuthProvider = ({
children, children,
@ -44,36 +124,122 @@ export const MockAuthProvider = ({
) )
} }
// Create a custom render function that includes providers // Enhanced provider wrapper with theme and notification contexts
const AllTheProviders = ({ children }: { children: React.ReactNode }) => { const AllTheProviders = ({
children,
authValues,
routerProps = {}
}: {
children: React.ReactNode
authValues?: Partial<MockAuthContextType>
routerProps?: any
}) => {
return ( return (
<BrowserRouter> <BrowserRouter {...routerProps}>
<MockAuthProvider> <MockAuthProvider mockValues={authValues}>
{children} {children}
</MockAuthProvider> </MockAuthProvider>
</BrowserRouter> </BrowserRouter>
) )
} }
// Enhanced render functions with better provider configuration
export const renderWithProviders = ( export const renderWithProviders = (
ui: React.ReactElement, ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'> options?: Omit<RenderOptions, 'wrapper'> & {
) => render(ui, { wrapper: AllTheProviders, ...options }) authValues?: Partial<MockAuthContextType>
routerProps?: any
}
) => {
const { authValues, routerProps, ...renderOptions } = options || {}
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<AllTheProviders authValues={authValues} routerProps={routerProps}>
{children}
</AllTheProviders>
)
return render(ui, { wrapper: Wrapper, ...renderOptions })
}
export const renderWithMockAuth = ( export const renderWithMockAuth = (
ui: React.ReactElement, ui: React.ReactElement,
mockAuthValues?: Partial<MockAuthContextType>, mockAuthValues?: Partial<MockAuthContextType>,
options?: Omit<RenderOptions, 'wrapper'> options?: Omit<RenderOptions, 'wrapper'>
) => { ) => {
const Wrapper = ({ children }: { children: React.ReactNode }) => ( return renderWithProviders(ui, { ...options, authValues: mockAuthValues })
<BrowserRouter> }
<MockAuthProvider mockValues={mockAuthValues}>
{children} // Render with authenticated user (commonly used pattern)
</MockAuthProvider> export const renderWithAuthenticatedUser = (
</BrowserRouter> ui: React.ReactElement,
) user: User = createMockUser(),
options?: Omit<RenderOptions, 'wrapper'>
) => {
return renderWithProviders(ui, {
...options,
authValues: {
user,
loading: false,
login: vi.fn(),
register: vi.fn(),
logout: vi.fn(),
}
})
}
// Render with admin user (commonly used pattern)
export const renderWithAdminUser = (
ui: React.ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) => {
return renderWithAuthenticatedUser(ui, createMockAdminUser(), options)
}
// Mock localStorage consistently across tests
export const createMockLocalStorage = () => {
const storage: Record<string, string> = {}
return render(ui, { wrapper: Wrapper, ...options }) return {
getItem: vi.fn((key: string) => storage[key] || null),
setItem: vi.fn((key: string, value: string) => { storage[key] = value }),
removeItem: vi.fn((key: string) => { delete storage[key] }),
clear: vi.fn(() => Object.keys(storage).forEach(key => delete storage[key])),
key: vi.fn((index: number) => Object.keys(storage)[index] || null),
length: Object.keys(storage).length,
}
}
// Setup function to be called in test setup files
export const setupTestEnvironment = () => {
// Mock localStorage
Object.defineProperty(window, 'localStorage', {
value: createMockLocalStorage(),
writable: true,
})
// Mock sessionStorage
Object.defineProperty(window, 'sessionStorage', {
value: createMockLocalStorage(),
writable: true,
})
// 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(),
})),
})
return setupApiMocks()
} }
// re-export everything // re-export everything

View File

@ -19,8 +19,6 @@ use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt};
use testcontainers_modules::postgres::Postgres; use testcontainers_modules::postgres::Postgres;
#[cfg(any(test, feature = "test-utils"))] #[cfg(any(test, feature = "test-utils"))]
use tower::util::ServiceExt; use tower::util::ServiceExt;
#[cfg(any(test, feature = "test-utils"))]
use uuid;
/// Test image information with expected OCR content /// Test image information with expected OCR content
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -152,163 +150,331 @@ mod tests {
} }
} }
/// Helper functions for integration tests /// Unified test context that eliminates duplication across integration tests
#[cfg(any(test, feature = "test-utils"))] #[cfg(any(test, feature = "test-utils"))]
pub async fn create_test_app() -> (Router, ContainerAsync<Postgres>) { pub struct TestContext {
let postgres_image = Postgres::default() pub app: Router,
.with_env_var("POSTGRES_USER", "test") pub container: ContainerAsync<Postgres>,
.with_env_var("POSTGRES_PASSWORD", "test") pub state: Arc<AppState>,
.with_env_var("POSTGRES_DB", "test");
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");
// Use TEST_DATABASE_URL if available, otherwise use the container
let database_url = std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| format!("postgresql://test:test@localhost:{}/test", port));
let db = crate::db::Database::new(&database_url).await.unwrap();
db.migrate().await.unwrap();
let config = crate::config::Config {
database_url: database_url.clone(),
server_address: "127.0.0.1:0".to_string(),
jwt_secret: "test-secret".to_string(),
upload_path: "./test-uploads".to_string(),
watch_folder: "./test-watch".to_string(),
allowed_file_types: vec!["pdf".to_string(), "txt".to_string(), "png".to_string()],
watch_interval_seconds: Some(30),
file_stability_check_ms: Some(500),
max_file_age_hours: None,
// OCR Configuration
ocr_language: "eng".to_string(),
concurrent_ocr_jobs: 2, // Lower for tests
ocr_timeout_seconds: 60, // Shorter for tests
max_file_size_mb: 10, // Smaller for tests
// Performance
memory_limit_mb: 256, // Lower for tests
cpu_priority: "normal".to_string(),
// OIDC Configuration
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
};
let queue_service = Arc::new(crate::ocr::queue::OcrQueueService::new(db.clone(), db.pool.clone(), 2));
let state = Arc::new(AppState {
db,
config,
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
});
let app = Router::new()
.nest("/api/auth", crate::routes::auth::router())
.nest("/api/documents", crate::routes::documents::router())
.nest("/api/search", crate::routes::search::router())
.nest("/api/settings", crate::routes::settings::router())
.nest("/api/users", crate::routes::users::router())
.nest("/api/ignored-files", crate::routes::ignored_files::ignored_files_routes())
.with_state(state);
(app, container)
} }
#[cfg(any(test, feature = "test-utils"))]
impl TestContext {
/// Create a new test context with default test configuration
pub async fn new() -> Self {
Self::with_config(TestConfigBuilder::default()).await
}
/// Create a test context with custom configuration
pub async fn with_config(config_builder: TestConfigBuilder) -> Self {
let postgres_image = Postgres::default()
.with_env_var("POSTGRES_USER", "test")
.with_env_var("POSTGRES_PASSWORD", "test")
.with_env_var("POSTGRES_DB", "test");
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 database_url = std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| format!("postgresql://test:test@localhost:{}/test", port));
let db = crate::db::Database::new(&database_url).await.unwrap();
db.migrate().await.unwrap();
let config = config_builder.build(database_url);
let queue_service = Arc::new(crate::ocr::queue::OcrQueueService::new(db.clone(), db.pool.clone(), 2));
let state = Arc::new(AppState {
db,
config,
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
});
let app = Router::new()
.nest("/api/auth", crate::routes::auth::router())
.nest("/api/documents", crate::routes::documents::router())
.nest("/api/search", crate::routes::search::router())
.nest("/api/settings", crate::routes::settings::router())
.nest("/api/users", crate::routes::users::router())
.nest("/api/ignored-files", crate::routes::ignored_files::ignored_files_routes())
.with_state(state.clone());
Self { app, container, state }
}
/// Get the app router for making requests
pub fn app(&self) -> &Router {
&self.app
}
/// Get the application state
pub fn state(&self) -> &Arc<AppState> {
&self.state
}
}
/// Builder pattern for test configuration to eliminate config duplication
#[cfg(any(test, feature = "test-utils"))]
pub struct TestConfigBuilder {
upload_path: String,
watch_folder: String,
jwt_secret: String,
concurrent_ocr_jobs: usize,
ocr_timeout_seconds: u64,
max_file_size_mb: u64,
memory_limit_mb: u64,
oidc_enabled: bool,
}
#[cfg(any(test, feature = "test-utils"))]
impl Default for TestConfigBuilder {
fn default() -> Self {
Self {
upload_path: "./test-uploads".to_string(),
watch_folder: "./test-watch".to_string(),
jwt_secret: "test-secret".to_string(),
concurrent_ocr_jobs: 2,
ocr_timeout_seconds: 60,
max_file_size_mb: 10,
memory_limit_mb: 256,
oidc_enabled: false,
}
}
}
#[cfg(any(test, feature = "test-utils"))]
impl TestConfigBuilder {
pub fn with_upload_path(mut self, path: &str) -> Self {
self.upload_path = path.to_string();
self
}
pub fn with_watch_folder(mut self, folder: &str) -> Self {
self.watch_folder = folder.to_string();
self
}
pub fn with_concurrent_ocr_jobs(mut self, jobs: usize) -> Self {
self.concurrent_ocr_jobs = jobs;
self
}
pub fn with_oidc_enabled(mut self, enabled: bool) -> Self {
self.oidc_enabled = enabled;
self
}
fn build(self, database_url: String) -> crate::config::Config {
crate::config::Config {
database_url,
server_address: "127.0.0.1:0".to_string(),
jwt_secret: self.jwt_secret,
upload_path: self.upload_path,
watch_folder: self.watch_folder,
allowed_file_types: vec!["pdf".to_string(), "txt".to_string(), "png".to_string()],
watch_interval_seconds: Some(30),
file_stability_check_ms: Some(500),
max_file_age_hours: None,
// OCR Configuration
ocr_language: "eng".to_string(),
concurrent_ocr_jobs: self.concurrent_ocr_jobs,
ocr_timeout_seconds: self.ocr_timeout_seconds,
max_file_size_mb: self.max_file_size_mb,
// Performance
memory_limit_mb: self.memory_limit_mb as usize,
cpu_priority: "normal".to_string(),
// OIDC Configuration
oidc_enabled: self.oidc_enabled,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
}
}
}
/// Legacy function for backward compatibility - will be deprecated
#[cfg(any(test, feature = "test-utils"))]
pub async fn create_test_app() -> (Router, ContainerAsync<Postgres>) {
let ctx = TestContext::new().await;
(ctx.app, ctx.container)
}
/// Unified test authentication helper that replaces TestClient/AdminTestClient patterns
#[cfg(any(test, feature = "test-utils"))]
pub struct TestAuthHelper {
app: Router,
}
#[cfg(any(test, feature = "test-utils"))]
impl TestAuthHelper {
pub fn new(app: Router) -> Self {
Self { app }
}
/// Create a regular test user with unique credentials
pub async fn create_test_user(&self) -> TestUser {
let test_id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
.to_string()[..8]
.to_string();
let username = format!("testuser_{}", test_id);
let email = format!("test_{}@example.com", test_id);
let password = "password123";
let user_data = json!({
"username": username,
"email": email,
"password": password
});
let response = self.make_request("POST", "/api/auth/register", Some(user_data), None).await;
let user_response: UserResponse = serde_json::from_slice(&response).unwrap();
TestUser {
user_response,
username,
password: password.to_string(),
token: None,
}
}
/// Create an admin test user with unique credentials
pub async fn create_admin_user(&self) -> TestUser {
let test_id = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
.to_string()[..8]
.to_string();
let username = format!("adminuser_{}", test_id);
let email = format!("admin_{}@example.com", test_id);
let password = "adminpass123";
let admin_data = json!({
"username": username,
"email": email,
"password": password,
"role": "admin"
});
let response = self.make_request("POST", "/api/auth/register", Some(admin_data), None).await;
let user_response: UserResponse = serde_json::from_slice(&response).unwrap();
TestUser {
user_response,
username,
password: password.to_string(),
token: None,
}
}
/// Login a user and return their authentication token
pub async fn login_user(&self, username: &str, password: &str) -> String {
let login_data = json!({
"username": username,
"password": password
});
let response = self.make_request("POST", "/api/auth/login", Some(login_data), None).await;
let login_response: serde_json::Value = serde_json::from_slice(&response).unwrap();
login_response["token"].as_str().unwrap().to_string()
}
/// Make an authenticated HTTP request
pub async fn make_authenticated_request(&self, method: &str, uri: &str, body: Option<serde_json::Value>, token: &str) -> Vec<u8> {
self.make_request(method, uri, body, Some(token)).await
}
/// Make an HTTP request (internal helper)
async fn make_request(&self, method: &str, uri: &str, body: Option<serde_json::Value>, token: Option<&str>) -> Vec<u8> {
let mut builder = axum::http::Request::builder()
.method(method)
.uri(uri)
.header("Content-Type", "application/json");
if let Some(token) = token {
builder = builder.header("Authorization", format!("Bearer {}", token));
}
let request_body = if let Some(body) = body {
axum::body::Body::from(serde_json::to_vec(&body).unwrap())
} else {
axum::body::Body::empty()
};
let response = self.app
.clone()
.oneshot(builder.body(request_body).unwrap())
.await
.unwrap();
axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap()
.to_vec()
}
}
/// Test user with authentication capabilities
#[cfg(any(test, feature = "test-utils"))]
pub struct TestUser {
pub user_response: UserResponse,
pub username: String,
pub password: String,
pub token: Option<String>,
}
#[cfg(any(test, feature = "test-utils"))]
impl TestUser {
/// Login this user and store the authentication token
pub async fn login(&mut self, auth_helper: &TestAuthHelper) -> Result<&str, Box<dyn std::error::Error>> {
let token = auth_helper.login_user(&self.username, &self.password).await;
self.token = Some(token);
Ok(self.token.as_ref().unwrap())
}
/// Make an authenticated request as this user
pub async fn make_request(&self, auth_helper: &TestAuthHelper, method: &str, uri: &str, body: Option<serde_json::Value>) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let token = self.token.as_ref().ok_or("User not logged in")?;
Ok(auth_helper.make_authenticated_request(method, uri, body, token).await)
}
/// Get user ID
pub fn id(&self) -> String {
self.user_response.id.to_string()
}
/// Check if user is admin
pub fn is_admin(&self) -> bool {
matches!(self.user_response.role, crate::models::UserRole::Admin)
}
}
/// Legacy functions for backward compatibility - will be deprecated
#[cfg(any(test, feature = "test-utils"))] #[cfg(any(test, feature = "test-utils"))]
pub async fn create_test_user(app: &Router) -> UserResponse { pub async fn create_test_user(app: &Router) -> UserResponse {
// Generate random identifiers to avoid test interference let auth_helper = TestAuthHelper::new(app.clone());
let test_id = uuid::Uuid::new_v4().to_string()[..8].to_string(); let test_user = auth_helper.create_test_user().await;
let test_username = format!("testuser_{}", test_id); test_user.user_response
let test_email = format!("test_{}@example.com", test_id);
let user_data = json!({
"username": test_username,
"email": test_email,
"password": "password123"
});
let response = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/api/auth/register")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&user_data).unwrap()))
.unwrap(),
)
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap()
} }
#[cfg(any(test, feature = "test-utils"))] #[cfg(any(test, feature = "test-utils"))]
pub async fn create_admin_user(app: &Router) -> UserResponse { pub async fn create_admin_user(app: &Router) -> UserResponse {
// Generate random identifiers to avoid test interference let auth_helper = TestAuthHelper::new(app.clone());
let test_id = uuid::Uuid::new_v4().to_string()[..8].to_string(); let admin_user = auth_helper.create_admin_user().await;
let admin_username = format!("adminuser_{}", test_id); admin_user.user_response
let admin_email = format!("admin_{}@example.com", test_id);
let admin_data = json!({
"username": admin_username,
"email": admin_email,
"password": "adminpass123",
"role": "admin"
});
let response = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/api/auth/register")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&admin_data).unwrap()))
.unwrap(),
)
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap()
} }
#[cfg(any(test, feature = "test-utils"))] #[cfg(any(test, feature = "test-utils"))]
pub async fn login_user(app: &Router, username: &str, password: &str) -> String { pub async fn login_user(app: &Router, username: &str, password: &str) -> String {
let login_data = json!({ let auth_helper = TestAuthHelper::new(app.clone());
"username": username, auth_helper.login_user(username, password).await
"password": password
});
let response = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/api/auth/login")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&login_data).unwrap()))
.unwrap(),
)
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let login_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
login_response["token"].as_str().unwrap().to_string()
} }

View File

@ -1,151 +0,0 @@
use crate::{AppState, models::UserResponse};
use axum::Router;
use serde_json::json;
use std::sync::Arc;
use testcontainers::{core::WaitFor, runners::AsyncRunner, ContainerAsync, GenericImage, ImageExt};
use testcontainers_modules::postgres::Postgres;
use tower::util::ServiceExt;
pub async fn create_test_app() -> (Router, ContainerAsync<Postgres>) {
let postgres_image = Postgres::default()
.with_tag("15-alpine")
.with_env_var("POSTGRES_USER", "test")
.with_env_var("POSTGRES_PASSWORD", "test")
.with_env_var("POSTGRES_DB", "test");
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 database_url = format!("postgresql://test:test@localhost:{}/test", port);
let db = crate::db::Database::new(&database_url).await.unwrap();
db.migrate().await.unwrap();
let config = crate::config::Config {
database_url: database_url.clone(),
server_address: "127.0.0.1:0".to_string(),
jwt_secret: "test-secret".to_string(),
upload_path: "./test-uploads".to_string(),
watch_folder: "./test-watch".to_string(),
allowed_file_types: vec!["pdf".to_string(), "txt".to_string(), "png".to_string()],
watch_interval_seconds: Some(30),
file_stability_check_ms: Some(500),
max_file_age_hours: None,
// OCR Configuration
ocr_language: "eng".to_string(),
concurrent_ocr_jobs: 2, // Lower for tests
ocr_timeout_seconds: 60, // Shorter for tests
max_file_size_mb: 10, // Smaller for tests
// Performance
memory_limit_mb: 256, // Lower for tests
cpu_priority: "normal".to_string(),
// OIDC Configuration (disabled for tests)
oidc_enabled: false,
oidc_client_id: None,
oidc_client_secret: None,
oidc_issuer_url: None,
oidc_redirect_uri: None,
};
let queue_service = Arc::new(crate::ocr::queue::OcrQueueService::new(db.clone(), db.pool.clone(), 2));
let state = Arc::new(AppState {
db,
config,
webdav_scheduler: None,
source_scheduler: None,
queue_service,
oidc_client: None,
});
let app = Router::new()
.nest("/api/auth", crate::routes::auth::router())
.nest("/api/documents", crate::routes::documents::router())
.nest("/api/search", crate::routes::search::router())
.nest("/api/settings", crate::routes::settings::router())
.nest("/api/users", crate::routes::users::router())
.with_state(state);
(app, container)
}
pub async fn create_test_user(app: &Router) -> UserResponse {
let user_data = json!({
"username": "testuser",
"email": "test@example.com",
"password": "password123"
});
let response = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/api/auth/register")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&user_data).unwrap()))
.unwrap(),
)
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap()
}
pub async fn create_admin_user(app: &Router) -> UserResponse {
let admin_data = json!({
"username": "adminuser",
"email": "admin@example.com",
"password": "adminpass123",
"role": "admin"
});
let response = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/api/auth/register")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&admin_data).unwrap()))
.unwrap(),
)
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
serde_json::from_slice(&body).unwrap()
}
pub async fn login_user(app: &Router, username: &str, password: &str) -> String {
let login_data = json!({
"username": username,
"password": password
});
let response = app
.clone()
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri("/api/auth/login")
.header("Content-Type", "application/json")
.body(axum::body::Body::from(serde_json::to_vec(&login_data).unwrap()))
.unwrap(),
)
.await
.unwrap();
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let login_response: serde_json::Value = serde_json::from_slice(&body).unwrap();
login_response["token"].as_str().unwrap().to_string()
}

View File

@ -1,4 +1,3 @@
mod helpers;
mod auth_tests; mod auth_tests;
mod config_oidc_tests; mod config_oidc_tests;
mod db_tests; mod db_tests;

View File

@ -1,7 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::models::UpdateSettings; use crate::models::UpdateSettings;
use super::super::helpers::{create_test_app, create_test_user, login_user}; use crate::test_utils::{create_test_app, create_test_user, login_user};
use axum::http::StatusCode; use axum::http::StatusCode;
use serde_json::json; use serde_json::json;
use tower::util::ServiceExt; use tower::util::ServiceExt;

View File

@ -1,7 +1,7 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::models::{CreateUser, UpdateUser, UserResponse, AuthProvider, UserRole}; use crate::models::{CreateUser, UpdateUser, UserResponse, AuthProvider, UserRole};
use super::super::helpers::{create_test_app, create_test_user, create_admin_user, login_user}; use crate::test_utils::{create_test_app, create_test_user, create_admin_user, login_user};
use axum::http::StatusCode; use axum::http::StatusCode;
use serde_json::json; use serde_json::json;
use tower::util::ServiceExt; use tower::util::ServiceExt;