From 14b29872e8854cecb9d86b09e45140468e73204e Mon Sep 17 00:00:00 2001 From: perf3ct Date: Thu, 3 Jul 2025 17:21:39 +0000 Subject: [PATCH] feat(tests): deduplicate tests too --- frontend/e2e/fixtures/auth.ts | 153 ++++-- frontend/e2e/ocr-retry-workflow.spec.ts | 218 +++++++++ frontend/e2e/webdav-workflow.spec.ts | 203 ++++++++ .../__tests__/GlobalSearchBar.test.tsx | 44 +- frontend/src/test/setup.ts | 53 +- frontend/src/test/test-utils.tsx | 194 +++++++- src/test_utils.rs | 462 ++++++++++++------ src/tests/helpers.rs | 151 ------ src/tests/mod.rs | 1 - src/tests/settings_tests.rs | 2 +- src/tests/users_tests.rs | 2 +- 11 files changed, 1061 insertions(+), 422 deletions(-) create mode 100644 frontend/e2e/ocr-retry-workflow.spec.ts create mode 100644 frontend/e2e/webdav-workflow.spec.ts delete mode 100644 src/tests/helpers.rs diff --git a/frontend/e2e/fixtures/auth.ts b/frontend/e2e/fixtures/auth.ts index fd4e342..ebff3ce 100644 --- a/frontend/e2e/fixtures/auth.ts +++ b/frontend/e2e/fixtures/auth.ts @@ -1,53 +1,124 @@ import { test as base, expect } 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 { 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({ authenticatedPage: async ({ page }, use) => { - await page.goto('/'); - - // Wait a bit for the page to load - await page.waitForLoadState('networkidle'); - - // Check if already logged in by looking for username input (login page) - const usernameInput = await page.locator('input[name="username"]').isVisible().catch(() => false); - - if (usernameInput) { - console.log('Found login form, attempting to login...'); - - // Fill login form with demo credentials - await page.fill('input[name="username"]', 'admin'); - await page.fill('input[name="password"]', 'readur2024'); - - // 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'); - } - + const auth = new AuthHelper(page); + await auth.loginAs(TEST_CREDENTIALS.admin); + await use(page); + }, + + adminPage: async ({ page }, use) => { + const auth = new AuthHelper(page); + await auth.loginAs(TEST_CREDENTIALS.admin); + await use(page); + }, + + userPage: async ({ page }, use) => { + const auth = new AuthHelper(page); + await auth.loginAs(TEST_CREDENTIALS.user); await use(page); }, }); diff --git a/frontend/e2e/ocr-retry-workflow.spec.ts b/frontend/e2e/ocr-retry-workflow.spec.ts new file mode 100644 index 0000000..43cd144 --- /dev/null +++ b/frontend/e2e/ocr-retry-workflow.spec.ts @@ -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`); + } + } + }); +}); \ No newline at end of file diff --git a/frontend/e2e/webdav-workflow.spec.ts b/frontend/e2e/webdav-workflow.spec.ts new file mode 100644 index 0000000..3971088 --- /dev/null +++ b/frontend/e2e/webdav-workflow.spec.ts @@ -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); + } + } + } + }); +}); \ No newline at end of file diff --git a/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx b/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx index ae222d1..470ecd9 100644 --- a/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx +++ b/frontend/src/components/GlobalSearchBar/__tests__/GlobalSearchBar.test.tsx @@ -1,18 +1,12 @@ 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 { BrowserRouter } from 'react-router-dom'; import { vi } from 'vitest'; import GlobalSearchBar from '../GlobalSearchBar'; +import { renderWithProviders, createMockApiServices } from '../../../test/test-utils'; -// Mock the API service -const mockDocumentService = { - enhancedSearch: vi.fn(), -}; - -vi.mock('../../../services/api', () => ({ - documentService: mockDocumentService, -})); +// Use centralized API mocking +const mockServices = createMockApiServices(); // Mock useNavigate 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 const mockSearchResponse = { data: { @@ -53,14 +38,7 @@ const mockSearchResponse = { } }; -// Helper to render component with router -const renderWithRouter = (component) => { - return render( - - {component} - - ); -}; +// Using centralized render utility (no custom helper needed) describe('GlobalSearchBar', () => { beforeEach(() => { @@ -70,7 +48,7 @@ describe('GlobalSearchBar', () => { }); test('renders search input with placeholder', () => { - renderWithRouter(); + renderWithProviders(); expect(screen.getByPlaceholderText('Search documents...')).toBeInTheDocument(); expect(screen.getByRole('textbox')).toBeInTheDocument(); @@ -78,7 +56,7 @@ describe('GlobalSearchBar', () => { test('accepts user input', async () => { const user = userEvent.setup(); - renderWithRouter(); + renderWithProviders(); const searchInput = screen.getByPlaceholderText('Search documents...'); await user.type(searchInput, 'test'); @@ -88,7 +66,7 @@ describe('GlobalSearchBar', () => { test('clears input when clear button is clicked', async () => { const user = userEvent.setup(); - renderWithRouter(); + renderWithProviders(); const searchInput = screen.getByPlaceholderText('Search documents...'); await user.type(searchInput, 'test'); @@ -101,7 +79,7 @@ describe('GlobalSearchBar', () => { }); test('shows popular searches when focused', async () => { - renderWithRouter(); + renderWithProviders(); const searchInput = screen.getByPlaceholderText('Search documents...'); fireEvent.focus(searchInput); @@ -112,7 +90,7 @@ describe('GlobalSearchBar', () => { }); test('handles empty search gracefully', () => { - renderWithRouter(); + renderWithProviders(); const searchInput = screen.getByPlaceholderText('Search documents...'); fireEvent.change(searchInput, { target: { value: '' } }); @@ -122,7 +100,7 @@ describe('GlobalSearchBar', () => { test('handles keyboard navigation', async () => { const user = userEvent.setup(); - renderWithRouter(); + renderWithProviders(); const searchInput = screen.getByPlaceholderText('Search documents...'); await user.type(searchInput, 'test query'); diff --git a/frontend/src/test/setup.ts b/frontend/src/test/setup.ts index 2286153..6518e4f 100644 --- a/frontend/src/test/setup.ts +++ b/frontend/src/test/setup.ts @@ -1,37 +1,26 @@ -import { expect, afterEach, vi } from 'vitest' -import { cleanup } from '@testing-library/react' -import * as matchers from '@testing-library/jest-dom/matchers' +// Global test setup file for Vitest +// This file is automatically loaded before all tests -expect.extend(matchers) +import '@testing-library/jest-dom' +import { vi } from 'vitest' +import { setupTestEnvironment } from './test-utils' -afterEach(() => { - cleanup() +// Setup global test environment +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 -vi.mock('axios', () => ({ - default: { - 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(), - })), +// Clean up after each test +afterEach(() => { + vi.clearAllMocks() }) \ No newline at end of file diff --git a/frontend/src/test/test-utils.tsx b/frontend/src/test/test-utils.tsx index 992b1ee..bd854e5 100644 --- a/frontend/src/test/test-utils.tsx +++ b/frontend/src/test/test-utils.tsx @@ -7,6 +7,7 @@ interface User { id: string username: string email: string + role?: string } interface MockAuthContextType { @@ -17,6 +18,85 @@ interface MockAuthContextType { logout: () => void } +// Test data factories for consistent mock data across tests +export const createMockUser = (overrides: Partial = {}): User => ({ + id: '1', + username: 'testuser', + email: 'test@example.com', + role: 'user', + ...overrides +}) + +export const createMockAdminUser = (overrides: Partial = {}): 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 export const MockAuthProvider = ({ children, @@ -44,36 +124,122 @@ export const MockAuthProvider = ({ ) } -// Create a custom render function that includes providers -const AllTheProviders = ({ children }: { children: React.ReactNode }) => { +// Enhanced provider wrapper with theme and notification contexts +const AllTheProviders = ({ + children, + authValues, + routerProps = {} +}: { + children: React.ReactNode + authValues?: Partial + routerProps?: any +}) => { return ( - - + + {children} ) } +// Enhanced render functions with better provider configuration export const renderWithProviders = ( ui: React.ReactElement, - options?: Omit -) => render(ui, { wrapper: AllTheProviders, ...options }) + options?: Omit & { + authValues?: Partial + routerProps?: any + } +) => { + const { authValues, routerProps, ...renderOptions } = options || {} + + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + + {children} + + ) + + return render(ui, { wrapper: Wrapper, ...renderOptions }) +} export const renderWithMockAuth = ( ui: React.ReactElement, mockAuthValues?: Partial, options?: Omit ) => { - const Wrapper = ({ children }: { children: React.ReactNode }) => ( - - - {children} - - - ) + return renderWithProviders(ui, { ...options, authValues: mockAuthValues }) +} + +// Render with authenticated user (commonly used pattern) +export const renderWithAuthenticatedUser = ( + ui: React.ReactElement, + user: User = createMockUser(), + options?: Omit +) => { + 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 +) => { + return renderWithAuthenticatedUser(ui, createMockAdminUser(), options) +} + +// Mock localStorage consistently across tests +export const createMockLocalStorage = () => { + const storage: Record = {} - 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 diff --git a/src/test_utils.rs b/src/test_utils.rs index aa01e89..abc670c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -19,8 +19,6 @@ use testcontainers::{runners::AsyncRunner, ContainerAsync, ImageExt}; use testcontainers_modules::postgres::Postgres; #[cfg(any(test, feature = "test-utils"))] use tower::util::ServiceExt; -#[cfg(any(test, feature = "test-utils"))] -use uuid; /// Test image information with expected OCR content #[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"))] -pub async fn create_test_app() -> (Router, ContainerAsync) { - 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"); - - // 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) +pub struct TestContext { + pub app: Router, + pub container: ContainerAsync, + pub state: Arc, } +#[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 { + &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) { + 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, token: &str) -> Vec { + 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, token: Option<&str>) -> Vec { + 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, +} + +#[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> { + 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) -> Result, Box> { + 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"))] pub async fn create_test_user(app: &Router) -> UserResponse { - // Generate random identifiers to avoid test interference - let test_id = uuid::Uuid::new_v4().to_string()[..8].to_string(); - let test_username = format!("testuser_{}", test_id); - 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() + let auth_helper = TestAuthHelper::new(app.clone()); + let test_user = auth_helper.create_test_user().await; + test_user.user_response } #[cfg(any(test, feature = "test-utils"))] pub async fn create_admin_user(app: &Router) -> UserResponse { - // Generate random identifiers to avoid test interference - let test_id = uuid::Uuid::new_v4().to_string()[..8].to_string(); - let admin_username = format!("adminuser_{}", test_id); - 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() + let auth_helper = TestAuthHelper::new(app.clone()); + let admin_user = auth_helper.create_admin_user().await; + admin_user.user_response } #[cfg(any(test, feature = "test-utils"))] 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() + let auth_helper = TestAuthHelper::new(app.clone()); + auth_helper.login_user(username, password).await } \ No newline at end of file diff --git a/src/tests/helpers.rs b/src/tests/helpers.rs deleted file mode 100644 index 816950b..0000000 --- a/src/tests/helpers.rs +++ /dev/null @@ -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) { - 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() -} \ No newline at end of file diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 0b4ff78..510a74c 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,4 +1,3 @@ -mod helpers; mod auth_tests; mod config_oidc_tests; mod db_tests; diff --git a/src/tests/settings_tests.rs b/src/tests/settings_tests.rs index 80049e6..9a928d9 100644 --- a/src/tests/settings_tests.rs +++ b/src/tests/settings_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { 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 serde_json::json; use tower::util::ServiceExt; diff --git a/src/tests/users_tests.rs b/src/tests/users_tests.rs index 80887ab..3474075 100644 --- a/src/tests/users_tests.rs +++ b/src/tests/users_tests.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod tests { use crate::models::{CreateUser, UpdateUser, UserResponse, AuthProvider, UserRole}; - use 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 serde_json::json; use tower::util::ServiceExt;