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