diff --git a/client/tests/internal/Errors.test.tsx b/client/tests/internal/Errors.test.tsx new file mode 100644 index 0000000..86bf35b --- /dev/null +++ b/client/tests/internal/Errors.test.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import Errors from '@/features/errors/Errors'; +import * as logRecords from '@/features/errors/log-records'; +import { toast } from 'sonner'; + +vi.mock('@/features/errors/log-records', () => ({ + getLogRecords: vi.fn(), + clearLogRecords: vi.fn(), +})); + +vi.mock('sonner', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +vi.mock('@/components/page-layout', () => ({ + PageLayout: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +describe('Errors Component', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders error log correctly', async () => { + vi.mocked(logRecords.getLogRecords).mockResolvedValue(['Error 1', 'Error 2']); + render(); + + await waitFor(() => { + expect(screen.getByText('Error log')).toBeInTheDocument(); + expect(screen.getByText('Error 1')).toBeInTheDocument(); + expect(screen.getByText('Error 2')).toBeInTheDocument(); + }); + }); + + it('handles empty error log', async () => { + vi.mocked(logRecords.getLogRecords).mockResolvedValue([]); + render(); + + await waitFor(() => { + expect(screen.getByText('No error log.')).toBeInTheDocument(); + }); + }); + + it('clears error log', async () => { + vi.mocked(logRecords.getLogRecords).mockResolvedValue(['Error 1']); + vi.mocked(logRecords.clearLogRecords).mockResolvedValue(); + + render(); + + await waitFor(() => { + expect(screen.getByText('Error 1')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Clear Log' })); + + const confirmButtons = screen.getAllByRole('button', { name: 'Clear Log' }); + fireEvent.click(confirmButtons[confirmButtons.length - 1]); + + await waitFor(() => { + expect(logRecords.clearLogRecords).toHaveBeenCalled(); + expect(toast.success).toHaveBeenCalledWith('Log cleared successfully'); + }); + }); + + it('handles clear log error', async () => { + vi.mocked(logRecords.getLogRecords).mockResolvedValue(['Error 1']); + vi.mocked(logRecords.clearLogRecords).mockRejectedValue(new Error('Clear failed')); + + render(); + + await waitFor(() => { + expect(screen.getByText('Error 1')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Clear Log' })); + + const confirmButtons = screen.getAllByRole('button', { name: 'Clear Log' }); + fireEvent.click(confirmButtons[confirmButtons.length - 1]); + + await waitFor(() => { + expect(logRecords.clearLogRecords).toHaveBeenCalled(); + expect(toast.error).toHaveBeenCalledWith('Failed to clear log'); + }); + }); +}); \ No newline at end of file diff --git a/client/tests/internal/Login.test.tsx b/client/tests/internal/Login.test.tsx new file mode 100644 index 0000000..cbf0d00 --- /dev/null +++ b/client/tests/internal/Login.test.tsx @@ -0,0 +1,89 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import Login from '@/features/user/Login'; +import * as authFunctions from '@/services/authfunctions'; +import { toast } from 'sonner'; + +vi.mock('@/services/authfunctions', () => ({ + loginUser: vi.fn(), +})); + +vi.mock('sonner', () => ({ + toast: { + error: vi.fn(), + success: vi.fn(), + }, +})); + +describe('Login Component', () => { + const mockOnLoginSuccess = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('renders login form correctly', () => { + render(); + + expect(screen.getByAltText('News Article Collector Logo')).toBeInTheDocument(); + expect(screen.getByText('News Article Collector')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Enter your password')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument(); + }); + + it('handles successful login', async () => { + const mockLoginResponse = { access_token: 'mock-token' }; + vi.mocked(authFunctions.loginUser).mockResolvedValue(mockLoginResponse); + + render(); + + const passwordInput = screen.getByPlaceholderText('Enter your password'); + const loginButton = screen.getByRole('button', { name: 'Log in' }); + + fireEvent.change(passwordInput, { target: { value: 'testpassword' } }); + fireEvent.click(loginButton); + + await waitFor(() => { + expect(authFunctions.loginUser).toHaveBeenCalledWith('testpassword'); + expect(toast.success).toHaveBeenCalledWith('Login successful!'); + expect(mockOnLoginSuccess).toHaveBeenCalled(); + }); + }); + + it('handles login failure', async () => { + vi.mocked(authFunctions.loginUser).mockResolvedValue(null); + + render(); + + const passwordInput = screen.getByPlaceholderText('Enter your password'); + const loginButton = screen.getByRole('button', { name: 'Log in' }); + + fireEvent.change(passwordInput, { target: { value: 'wrongpassword' } }); + fireEvent.click(loginButton); + + await waitFor(() => { + expect(authFunctions.loginUser).toHaveBeenCalledWith('wrongpassword'); + expect(toast.error).toHaveBeenCalledWith('Failed to login'); + expect(mockOnLoginSuccess).not.toHaveBeenCalled(); + }); + }); + + it('handles login error', async () => { + vi.mocked(authFunctions.loginUser).mockRejectedValue(new Error('Network error')); + + render(); + + const passwordInput = screen.getByPlaceholderText('Enter your password'); + const loginButton = screen.getByRole('button', { name: 'Log in' }); + + fireEvent.change(passwordInput, { target: { value: 'testpassword' } }); + fireEvent.click(loginButton); + + await waitFor(() => { + expect(authFunctions.loginUser).toHaveBeenCalledWith('testpassword'); + expect(toast.error).toHaveBeenCalledWith('Login failed: Network error'); + expect(mockOnLoginSuccess).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/client/tests/internal/Register.test.tsx b/client/tests/internal/Register.test.tsx new file mode 100644 index 0000000..cb6a1bd --- /dev/null +++ b/client/tests/internal/Register.test.tsx @@ -0,0 +1,90 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { BrowserRouter } from 'react-router-dom'; +import Register from '@/features/user/Register'; +import * as authFunctions from '@/services/authfunctions'; +import { toast } from 'sonner'; + +vi.mock('@/services/authfunctions', () => ({ + registerUser: vi.fn(), +})); + +vi.mock('sonner', () => ({ + toast: { + error: vi.fn(), + success: vi.fn(), + warning: vi.fn(), + }, +})); + +describe('Register Component', () => { + const mockOnRegistrationSuccess = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const renderRegister = () => { + return render( + + + + ); + }; + + it('renders register form correctly', () => { + renderRegister(); + expect(screen.getByAltText('News Article Collector Logo')).toBeInTheDocument(); + expect(screen.getByText('News Article Collector')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Enter your email')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Enter your password')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Register' })).toBeInTheDocument(); + }); + + it('handles successful registration', async () => { + vi.mocked(authFunctions.registerUser).mockResolvedValue({ success: true, data: {}, emailSent: true }); + renderRegister(); + + fireEvent.change(screen.getByPlaceholderText('Enter your email'), { target: { value: 'test@example.com' } }); + fireEvent.change(screen.getByPlaceholderText('Enter your password'), { target: { value: 'password123' } }); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); + + await waitFor(() => { + expect(authFunctions.registerUser).toHaveBeenCalledWith('test@example.com', 'password123', false); + expect(toast.success).toHaveBeenCalledWith('Registration successful!'); + expect(mockOnRegistrationSuccess).toHaveBeenCalled(); + }); + }); + + it('handles registration failure', async () => { + vi.mocked(authFunctions.registerUser).mockResolvedValue({ success: false, error: 'Registration failed' }); + renderRegister(); + + fireEvent.change(screen.getByPlaceholderText('Enter your email'), { target: { value: 'test@example.com' } }); + fireEvent.change(screen.getByPlaceholderText('Enter your password'), { target: { value: 'password123' } }); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); + + await waitFor(() => { + expect(authFunctions.registerUser).toHaveBeenCalledWith('test@example.com', 'password123', false); + expect(toast.error).toHaveBeenCalledWith('Failed to register: Registration failed'); + expect(mockOnRegistrationSuccess).not.toHaveBeenCalled(); + }); + }); + + it('handles registration with email sending issue', async () => { + vi.mocked(authFunctions.registerUser).mockResolvedValue({ success: true, data: {}, emailSent: false }); + renderRegister(); + + fireEvent.change(screen.getByPlaceholderText('Enter your email'), { target: { value: 'test@example.com' } }); + fireEvent.change(screen.getByPlaceholderText('Enter your password'), { target: { value: 'password123' } }); + fireEvent.click(screen.getByRole('button', { name: 'Register' })); + + await waitFor(() => { + expect(authFunctions.registerUser).toHaveBeenCalledWith('test@example.com', 'password123', false); + expect(toast.success).toHaveBeenCalledWith('Registration successful!'); + expect(toast.warning).toHaveBeenCalledWith('Registration successful, but there was an issue sending the confirmation email. You can still use the app.', { duration: 10000 }); + expect(mockOnRegistrationSuccess).toHaveBeenCalled(); + }); + }); +}); diff --git a/client/tests/internal/ReregisterValidator.test.tsx b/client/tests/internal/ReregisterValidator.test.tsx new file mode 100644 index 0000000..491ffbd --- /dev/null +++ b/client/tests/internal/ReregisterValidator.test.tsx @@ -0,0 +1,74 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import ReregisterValidator from '@/features/user/ReregisterValidator'; +import * as authFunctions from '@/services/authfunctions'; +import { toast } from 'sonner'; + +vi.mock('@/services/authfunctions', () => ({ + validateReregisterToken: vi.fn(), +})); + +vi.mock('sonner', () => ({ + toast: { + error: vi.fn(), + custom: vi.fn(), + }, +})); + +describe('ReregisterValidator Component', () => { + const mockOnValidationComplete = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const renderReregisterValidator = (token: string) => { + return render( + + + } /> + + + ); + }; + + it('displays loading message while validating', () => { + renderReregisterValidator('valid-token'); + expect(screen.getByText('Validating reregistration token...')).toBeInTheDocument(); + }); + + it('handles valid token', async () => { + vi.mocked(authFunctions.validateReregisterToken).mockResolvedValue({ valid: true }); + renderReregisterValidator('valid-token'); + + await waitFor(() => { + expect(authFunctions.validateReregisterToken).toHaveBeenCalledWith('valid-token'); + expect(toast.custom).toHaveBeenCalled(); + expect(mockOnValidationComplete).toHaveBeenCalledWith(true); + }); + }); + + it('handles invalid token', async () => { + vi.mocked(authFunctions.validateReregisterToken).mockResolvedValue({ valid: false }); + renderReregisterValidator('invalid-token'); + + await waitFor(() => { + expect(authFunctions.validateReregisterToken).toHaveBeenCalledWith('invalid-token'); + expect(toast.error).toHaveBeenCalledWith('Invalid or expired token'); + expect(mockOnValidationComplete).toHaveBeenCalledWith(false); + }); + }); + + it('handles validation error', async () => { + vi.mocked(authFunctions.validateReregisterToken).mockRejectedValue(new Error('Validation failed')); + renderReregisterValidator('error-token'); + + await waitFor(() => { + expect(authFunctions.validateReregisterToken).toHaveBeenCalledWith('error-token'); + expect(toast.error).toHaveBeenCalledWith('An error occurred when validating the token. Did it expire already?'); + expect(mockOnValidationComplete).toHaveBeenCalledWith(false); + }); + }); +}); diff --git a/client/tests/internal/advanced-search.test.tsx b/client/tests/internal/advanced-search.test.tsx new file mode 100644 index 0000000..eb5534c --- /dev/null +++ b/client/tests/internal/advanced-search.test.tsx @@ -0,0 +1,71 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import AdvancedSearch from '@/features/search/advanced-search'; +import { SearchProvider } from '@/features/search/search-context'; + +vi.mock('sonner', () => ({ + toast: { + info: vi.fn(), + dismiss: vi.fn(), + }, +})); + +describe('AdvancedSearch Component', () => { + const mockOnSearchParamsChange = vi.fn(); + const mockOnSearch = vi.fn(); + const mockOnDownload = vi.fn(); + const mockOnClear = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + const renderAdvancedSearch = () => { + return render( + + + + ); +}; + + it('renders basic search form correctly', () => { + renderAdvancedSearch(); + expect(screen.getByPlaceholderText('Insert query...')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Search' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Advanced' })).toBeInTheDocument(); + }); + + it('toggles advanced search options', async () => { + renderAdvancedSearch(); + fireEvent.click(screen.getByRole('button', { name: 'Advanced' })); + + await waitFor(() => { + expect(screen.getByPlaceholderText('Insert text query...')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Insert URL query...')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Insert HTML query...')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Insert start time... (YYYY-MM-DD HH:MM:SS)')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Insert end time... (YYYY-MM-DD HH:MM:SS)')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Advanced' })); + + await waitFor(() => { + expect(screen.queryByPlaceholderText('Insert text query...')).not.toBeInTheDocument(); + }); + }); + + it('handles clear button click', () => { + renderAdvancedSearch(); + fireEvent.click(screen.getByRole('button', { name: 'Clear' })); + expect(mockOnClear).toHaveBeenCalled(); + }); +}); diff --git a/client/tests/internal/article-downloads.test.tsx b/client/tests/internal/article-downloads.test.tsx new file mode 100644 index 0000000..207b224 --- /dev/null +++ b/client/tests/internal/article-downloads.test.tsx @@ -0,0 +1,73 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { handleArticleDownload } from '@/services/article-download'; +import { toast } from 'sonner'; + +vi.mock('@/services/authclient', () => ({ + default: { + get: vi.fn(() => Promise.resolve({ data: new Blob(['test data']) })), + }, +})); + +vi.mock('sonner', () => ({ + toast: { + dismiss: vi.fn(), + promise: vi.fn((promiseFn) => promiseFn()), + }, +})); + +describe('handleArticleDownload', () => { + const mockSetIsDisabled = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + global.URL.createObjectURL = vi.fn(() => 'mock-url'); + global.URL.revokeObjectURL = vi.fn(); + + document.createElement = vi.fn(() => { + const element = { + click: vi.fn(), + setAttribute: vi.fn(), + style: {}, + remove: vi.fn(), + }; + + return new Proxy(element, { + get: (target, prop) => { + if (prop in target) { + return target[prop as keyof typeof target]; + } + if (typeof prop === 'string' && !['toString', 'valueOf'].includes(prop)) { + return vi.fn(); + } + return undefined; + }, + }) as unknown as HTMLElement; + }); + + document.body.appendChild = vi.fn(); + document.body.removeChild = vi.fn(); + }); + + it('should toggle disabled state and trigger toast', async () => { + await handleArticleDownload('json', false, mockSetIsDisabled); + + expect(mockSetIsDisabled).toHaveBeenCalledWith(true); + expect(mockSetIsDisabled).toHaveBeenCalledWith(false); + expect(toast.promise).toHaveBeenCalled(); + }); + + it('should handle different file formats', async () => { + await handleArticleDownload('csv', false, mockSetIsDisabled); + await handleArticleDownload('parquet', false, mockSetIsDisabled); + + expect(mockSetIsDisabled).toHaveBeenCalledTimes(4); + expect(toast.promise).toHaveBeenCalledTimes(2); + }); + + it('should handle query exports', async () => { + await handleArticleDownload('json', true, mockSetIsDisabled); + + expect(mockSetIsDisabled).toHaveBeenCalledTimes(2); + expect(toast.promise).toHaveBeenCalled(); + }); +}); diff --git a/client/tests/internal/authfunctions.test.tsx b/client/tests/internal/authfunctions.test.tsx new file mode 100644 index 0000000..ef3cb5e --- /dev/null +++ b/client/tests/internal/authfunctions.test.tsx @@ -0,0 +1,49 @@ +import { checkUserExists, registerUser, loginUser, getIsValidToken } from '@/services/authfunctions'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import axios from 'axios'; +import authClient from '@/services/authclient'; + +vi.mock('axios'); +vi.mock('@/services/authclient', () => ({ + default: { + get: vi.fn(), + }, +})); + +describe('Auth Functions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should check if a user exists', async () => { + vi.mocked(axios.get).mockResolvedValueOnce({ data: { exists: true } }); + + const result = await checkUserExists(); + expect(result.exists).toBe(true); + expect(axios.get).toHaveBeenCalledWith(expect.stringContaining('/api/get_user_exists')); + }); + + it('should register a user successfully', async () => { + const response = { message: 'User registered successfully', email_sent: true }; + vi.mocked(axios.post).mockResolvedValueOnce({ data: response }); + + const result = await registerUser('email@test.com', 'password'); + expect(result.success).toBe(true); + expect(result.data).toEqual(response); + }); + + it('should handle login user success', async () => { + const response = { access_token: 'mock-access-token' }; + vi.mocked(axios.post).mockResolvedValueOnce({ data: response }); + + const result = await loginUser('password'); + expect(result.access_token).toBe('mock-access-token'); + }); + + it('should validate token correctly', async () => { + vi.mocked(authClient.get).mockResolvedValueOnce({ data: { valid: true } }); + + const result = await getIsValidToken(); + expect(result).toBe(true); + }); +}); \ No newline at end of file diff --git a/client/tests/internal/database-queries.test.tsx b/client/tests/internal/database-queries.test.tsx new file mode 100644 index 0000000..17b61f3 --- /dev/null +++ b/client/tests/internal/database-queries.test.tsx @@ -0,0 +1,31 @@ +import { sendSearchQuery, sendStatisticsQuery } from '@/services/database-queries'; +import authClient from '@/services/authclient'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; + +vi.mock('@/services/authclient', () => ({ + default: { + get: vi.fn(), + }, +})); + +describe('Database Queries', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should send search query successfully', async () => { + const mockResponse = { data: [], total_count: 10, page: 1 }; + vi.mocked(authClient.get).mockResolvedValueOnce({ data: mockResponse }); + + const response = await sendSearchQuery({ generalQuery: 'test' }); + expect(response).toEqual(mockResponse); + }); + + it('should handle statistics query', async () => { + const mockResponse = { data: { count: 100 } }; + vi.mocked(authClient.get).mockResolvedValueOnce({ data: mockResponse }); + + const response = await sendStatisticsQuery(true); + expect(response).toEqual(mockResponse); + }); +}); diff --git a/client/tests/internal/highlighted-text.test.tsx b/client/tests/internal/highlighted-text.test.tsx new file mode 100644 index 0000000..6b441b8 --- /dev/null +++ b/client/tests/internal/highlighted-text.test.tsx @@ -0,0 +1,17 @@ +import { render } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { describe, it, expect } from 'vitest'; +import { HighlightedText } from '@/components/ui/highlighted-text'; + +describe('HighlightedText Component', () => { + it('renders text with no highlights if no match is found', () => { + const { container } = render(); + expect(container.textContent).toBe('Hello World'); + expect(container.querySelector('mark')).toBeNull(); + }); + + it('highlights text when match is found', () => { + const { container } = render(); + expect(container.querySelector('mark')?.textContent).toBe('World'); + }); +}); diff --git a/server/tests/test_views/test_query_processor.py b/server/tests/test_views/test_query_processor.py index 9e7986b..7b90fea 100644 --- a/server/tests/test_views/test_query_processor.py +++ b/server/tests/test_views/test_query_processor.py @@ -84,3 +84,63 @@ def test_get_search_results_sorting(client): assert 'data' in response.json if len(response.json['data']) > 1: assert response.json['data'][0]['time'] >= response.json['data'][1]['time'] + +@pytest.mark.usefixtures("setup_and_teardown") +def test_get_search_results_with_text_query(client): + """ + Tests searching with a specific text query. + """ + response = client.get('/api/articles/search', query_string={'textQuery': 'Full text'}) + assert response.status_code == 200 + assert isinstance(response.json, dict) + assert 'data' in response.json + assert all('Full text' in item['full_text'] for item in response.json['data']) + +@pytest.mark.usefixtures("setup_and_teardown") +def test_get_search_results_with_url_query(client): + """ + Tests searching with a specific URL query. + """ + response = client.get('/api/articles/search', query_string={'urlQuery': 'blabla.com'}) + assert response.status_code == 200 + assert isinstance(response.json, dict) + assert 'data' in response.json + assert all('blabla.com' in item['url'] for item in response.json['data']) + +@pytest.mark.usefixtures("setup_and_teardown") +def test_get_search_results_with_time_range(client): + """ + Tests searching within a specific time range. + """ + response = client.get('/api/articles/search', query_string={ + 'startTime': '2016-01-01', + 'endTime': '2016-12-31' + }) + assert response.status_code == 200 + assert isinstance(response.json, dict) + assert 'data' in response.json + assert all('2016' in item['time'] for item in response.json['data']) + +@pytest.mark.usefixtures("setup_and_teardown") +def test_get_search_results_with_advanced_query(client): + """ + Tests searching with an advanced query using AND, OR, NOT operators. + """ + response = client.get('/api/articles/search', query_string={ + 'textQuery': 'Full text AND (1 OR 2) NOT 3' + }) + assert response.status_code == 200 + assert isinstance(response.json, dict) + assert 'data' in response.json + +@pytest.mark.usefixtures("setup_and_teardown") +def test_get_search_results_with_escaped_characters(client): + """ + Tests searching with escaped characters in the query. + """ + response = client.get('/api/articles/search', query_string={ + 'textQuery': 'ESC%Full ESC_text' + }) + assert response.status_code == 200 + assert isinstance(response.json, dict) + assert 'data' in response.json diff --git a/server/tests/test_views/test_reregistration.py b/server/tests/test_views/test_reregistration.py new file mode 100644 index 0000000..1e09033 --- /dev/null +++ b/server/tests/test_views/test_reregistration.py @@ -0,0 +1,56 @@ +""" +Test reregistration.py. +""" +from unittest.mock import patch +import pytest +from itsdangerous import URLSafeTimedSerializer + +@pytest.mark.usefixtures("setup_and_teardown") +def test_request_reregister_user_not_found(client): + """ + Test reregister user not found. + """ + with patch('src.views.administration.reregistration.get_user_data', return_value=None): + response = client.post('/api/request_reregister') + assert response.status_code == 404 + assert response.json['msg'] == "User does not exist" + +@pytest.mark.usefixtures("setup_and_teardown") +def test_request_reregister_success(client): + """ + Test reregister success. + """ + with patch( + 'src.views.administration.reregistration.get_user_data', + return_value={'email': 'test@example.com'} + ): + response = client.post('/api/request_reregister') + + assert response.status_code == 200 + assert 'msg' in response.json + assert response.json['msg'] == "Reregistration link generated" + assert 'reregister_link' in response.json + assert response.json['reregister_link'].startswith('http') + assert '/reregister/' in response.json['reregister_link'] + +@pytest.mark.usefixtures("setup_and_teardown") +def test_validate_reregister_token_valid(app, client): + """ + Test reregister valid token. + """ + serializer = URLSafeTimedSerializer(app.config['REREGISTER_SECRET_KEY']) + token = serializer.dumps('test@example.com') + + response = client.get(f'/api/validate_reregister_token/{token}') + assert response.status_code == 200 + assert response.json['valid'] is True + assert response.json['email'] == 'test@example.com' + +@pytest.mark.usefixtures("setup_and_teardown") +def test_validate_reregister_token_invalid(client): + """ + Test reregister invalid token. + """ + response = client.get('/api/validate_reregister_token/invalid_token') + assert response.status_code == 400 + assert response.json['valid'] is False diff --git a/server/tests/test_views/test_stats_analyzer.py b/server/tests/test_views/test_stats_analyzer.py index 1f5eb53..aa8bb32 100644 --- a/server/tests/test_views/test_stats_analyzer.py +++ b/server/tests/test_views/test_stats_analyzer.py @@ -14,6 +14,7 @@ def test_get_stats(client): response = client.get('/api/articles/statistics') assert response.status_code == 200 assert isinstance(response.json, list) + assert len(response.json) == 3 def test_get_stats_no_articles(client): """ @@ -34,3 +35,53 @@ def test_get_stats_db_error(client): assert response.json['message'] == ( "Database error when getting statistics: Mock database error" ) + +@pytest.mark.usefixtures("setup_and_teardown") +def test_get_text(client): + """ + Tests getting full text of articles. + """ + response = client.get('/api/articles/full_text') + assert response.status_code == 200 + assert isinstance(response.json, list) + assert all('full_text' in item for item in response.json) + +def test_get_text_db_error(client): + """Tests db error when getting full text.""" + with patch('src.utils.resource_management.inspect') as mock_inspect: + mock_inspect.side_effect = SQLAlchemyError("Mock database error") + + response = client.get('/api/articles/full_text') + assert response.status_code == 500 + assert response.json['message'] == ( + "Database error when getting text fields: Mock database error" + ) + +def test_get_data_size(client): + """ + Tests getting the size of the data.db file. + """ + with patch('os.path.exists', return_value=True), \ + patch('os.path.getsize', return_value=1024 * 1024): # 1 MB + response = client.get('/api/data_size') + assert response.status_code == 200 + assert response.json['size'] == "1.00 MB" + +def test_get_data_size_no_file(client): + """ + Tests getting the size when data.db doesn't exist. + """ + with patch('os.path.exists', return_value=False): + response = client.get('/api/data_size') + assert response.status_code == 200 + assert response.json['size'] == "0 bytes" + +def test_get_data_size_error(client): + """ + Tests error handling when getting data size fails. + """ + with patch('os.path.exists', side_effect=Exception("Test error")): + response = client.get('/api/data_size') + assert response.status_code == 500 + assert response.json['status'] == "error" + assert "Test error" in response.json['message']