refactoring integration tests

This commit is contained in:
2025-10-16 22:46:49 +02:00
parent ef88d2925e
commit a406440126
10 changed files with 314 additions and 233 deletions

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ tests/TestDBEngineRoot
tests/*.png
src/*.png
tests/*.html
tests/*.txt
test-results
.sesskey
tools.db

View File

@@ -49,6 +49,9 @@ clean:
rm -rf src/tools.db
rm -rf src/*.out
rm -rf src/*.prof
rm -rf tests/debug*.txt
rm -rf tests/debug*.html
rm -rf tests/debug*.png
find . -name '.sesskey' -exec rm -rf {} +
find . -name '.pytest_cache' -exec rm -rf {} +
find . -name '__pycache__' -exec rm -rf {} +

View File

@@ -13,6 +13,10 @@ load_dotenv()
DB_PATH = os.getenv("DB_PATH", "tools.db")
logger.info(f"{DB_PATH=}")
# Custom database engine settings
DBENGINE_PATH = os.getenv("DBENGINE_PATH", ".mytools_db")
logger.info(f"{DBENGINE_PATH=}")
# Authentication settings
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")

View File

@@ -1,6 +1,7 @@
import logging
from datetime import datetime
import config
from constants import NOT_LOGGED, NO_SESSION
from core.dbengine import DbEngine, TAG_PARENT, TAG_USER, TAG_DATE, DbException
from core.settings_objects import *
@@ -92,7 +93,7 @@ class MemoryDbEngine:
class SettingsManager:
def __init__(self, engine=None):
self._db_engine = engine or DbEngine()
self._db_engine = engine or DbEngine(config.DBENGINE_PATH)
def save(self, session: dict, entry: str, obj: object):
user_id, user_email = self._get_user(session)
@@ -263,3 +264,15 @@ class NestedSettingsManager:
raise AttributeError(f"Settings '{self._obj_entry}' has no attribute '{self._obj_attribute}'.")
return settings, getattr(settings, self._obj_attribute)
_settings_manager = SettingsManager()
def set_settings_manager(_setting_manager):
global _settings_manager
_settings_manager = _setting_manager
def get_settings_manager():
return _settings_manager

View File

@@ -20,10 +20,10 @@ from components.register.constants import ROUTE_ROOT as REGISTER_ROUTE_ROOT
from components.register.constants import Routes as RegisterRoutes
from config import APP_PORT
from constants import Routes
from core.dbengine import DbException
from core.database_manager import initialize_database
from core.dbengine import DbException
from core.instance_manager import InstanceManager
from core.settings_management import SettingsManager
from core.settings_management import get_settings_manager
from pages.admin_import_settings import AdminImportSettings, IMPORT_SETTINGS_PATH, import_settings_app
from pages.another_grid import get_datagrid2
from pages.basic_test import BASIC_TEST_PATH, basic_test_app, get_basic_test
@@ -236,9 +236,7 @@ async def timing_middleware(request, call_next):
return response
settings_manager = SettingsManager()
import_settings = AdminImportSettings(settings_manager, None)
import_settings = AdminImportSettings(get_settings_manager(), None)
pages = [
Page("My table", get_datagrid, id="my_table"),
Page("new settings", import_settings, id="import_settings"),
@@ -247,8 +245,8 @@ pages = [
Page("Another Table", get_datagrid2, id="another_table"),
]
login = Login(settings_manager)
register = Register(settings_manager)
login = Login(get_settings_manager())
register = Register(get_settings_manager())
InstanceManager.register_many(login, register)
@@ -258,8 +256,8 @@ def get(session):
main = InstanceManager.get(session,
DrawerLayout.create_component_id(session),
DrawerLayout,
settings_manager=settings_manager)
return page_layout_lite(session, settings_manager, main)
settings_manager=get_settings_manager())
return page_layout_lite(session, get_settings_manager(), main)
except DbException:
return RedirectResponse(LOGIN_ROUTE_ROOT + LoginRoutes.Logout, status_code=303)
@@ -303,7 +301,7 @@ def not_found(path: str, session=None):
return page_layout_new(
session=session,
settings_manager=settings_manager,
settings_manager=get_settings_manager(),
content=error_content
)
@@ -317,6 +315,7 @@ def main(port):
logger.info(f" Starting FastHTML server on http://localhost:{port}")
serve(port=port)
if __name__ == "__main__":
# Start your application
logger.info("Application starting...")

View File

@@ -1,13 +1,15 @@
from io import BytesIO
import os
import shutil
import subprocess
import sys
import tempfile
import time
from io import BytesIO
from pathlib import Path
import pandas as pd
import pytest
import requests
import time
import sys
import os
from pathlib import Path
from components.datagrid.DataGrid import reset_instances
from playwright_config import BROWSER_CONFIG, BASE_URL
@@ -18,6 +20,7 @@ USER_EMAIL = "test@mail.com"
USER_ID = "test_user"
APP_PORT = 5002
@pytest.fixture(scope="session")
def test_database():
"""Configure temporary database for tests"""
@@ -57,7 +60,19 @@ def test_users():
@pytest.fixture(scope="session")
def app_server(test_database, test_users):
def test_setting_manager():
# create a dedicated folder for the db
db_engine_folder = tempfile.mkdtemp(prefix="test_db_engine")
yield db_engine_folder
# clean up folder & restore settings manager
if os.path.exists(db_engine_folder):
shutil.rmtree(db_engine_folder, ignore_errors=True)
@pytest.fixture(scope="session")
def app_server(test_database, test_users, test_setting_manager):
"""Start application server with test database"""
# Use the same Python executable that's running pytest
python_executable = sys.executable
@@ -69,6 +84,7 @@ def app_server(test_database, test_users):
# Create test environment
test_env = os.environ.copy()
test_env["DB_PATH"] = test_database
test_env["DBENGINE_PATH"] = test_setting_manager
test_env["ADMIN_EMAIL"] = test_users["admin"]["email"]
test_env["ADMIN_PASSWORD"] = test_users["admin"]["password"]
test_env["SECRET_KEY"] = "test-secret-key-for-e2e-tests"

View File

@@ -1,10 +1,13 @@
# README - End-to-End Authentication Testing Guide
This README provides details on how to set up, configure, and run the end-to-end (e2e) authentication tests for the **My Managing Tools** application. The tests are implemented using `pytest` with `playwright` for browser automation.
This README provides details on how to set up, configure, and run the end-to-end (e2e) authentication tests for the **My
Managing Tools** application. The tests are implemented using `pytest` with `playwright` for browser automation.
## Purpose
The e2e tests verify that the authentication system works as expected, including login functionality, session validation, and database isolation for testing purposes. These tests ensure the login page behaves properly under different scenarios such as unauthenticated access, form validation, and successful user login.
The e2e tests verify that the authentication system works as expected, including login functionality, session
validation, and database isolation for testing purposes. These tests ensure the login page behaves properly under
different scenarios such as unauthenticated access, form validation, and successful user login.
---
@@ -21,52 +24,6 @@ The e2e tests verify that the authentication system works as expected, including
---
## Setup Instructions
Before running the tests, make sure the environment is set up correctly for both the application and the test database.
### Application Setup
1. **Install Required Dependencies** (Python version 3.9 or higher is recommended):
```bash
pip install -r requirements.txt
```
2. **Run the Application**:
Navigate to the `src` directory and start the application:
```bash
cd src
python main.py
```
Alternatively, use Docker:
```bash
docker-compose up -d
```
Once the server starts, ensure the app is accessible at the base URL defined in `playwright_config.py`. By default, it should be `http://localhost:5002`.
### Database Setup
The e2e tests are designed to interact with an **isolated test database**. During setup, a temporary SQLite database will be created for each test session.
1. **Test Database Initialization**:
The database is initialized dynamically during the test execution using a custom temporary path. The `test_database` fixture creates a database with the following characteristics:
- **Temporary Directory**: Ensures no conflicts with the production database.
- **Admin User Auto-Creation**: Adds an admin account if the environment variables `ADMIN_EMAIL` and `ADMIN_PASSWORD` are set.
2. **Environment Variables**:
Ensure the following environment variables are defined for the test database:
```env
DB_PATH="test_mmt_tools.db"
ADMIN_EMAIL="test.admin@test.com"
ADMIN_PASSWORD="TestAdmin123"
```
3. **Configuration**:
The `test_database.py` script handles the database isolation and cleanup after each test session. It ensures that no state is leaked between tests.
---
## Installing Dependencies
Install the required packages for the tests:
@@ -93,6 +50,29 @@ Install the required packages for the tests:
---
### Database Setup
The e2e tests are designed to interact with an **isolated test database**. During setup, a temporary SQLite database will be created for each test session.
1. **Test Database Initialization**:
The database is initialized dynamically during the test execution using a custom temporary path. The `test_database` fixture creates a database with the following characteristics:
- **Temporary Directory**: Ensures no conflicts with the production database.
- **Admin User Auto-Creation**: Adds an admin account if the environment variables `ADMIN_EMAIL` and `ADMIN_PASSWORD` are set.
2. **Environment Variables**:
Ensure the following environment variables are defined for the test database:
```env
DB_PATH="test_mmt_tools.db"
ADMIN_EMAIL="test.admin@test.com"
ADMIN_PASSWORD="TestAdmin123"
```
3. **Configuration**:
The `test_database.py` script handles the database isolation and cleanup after each test session. It ensures that no state is leaked between tests.
---
## Running Tests
To execute the end-to-end authentication tests, run the following commands from the project root.
@@ -126,7 +106,8 @@ To execute the end-to-end authentication tests, run the following commands from
## Debugging and Logs
1. **Console Logs**:
Console errors during page navigation are captured using the `test_page_loads_without_errors` test. Check the pytest output for details.
Console errors during page navigation are captured using the `test_page_loads_without_errors` test. Check the pytest
output for details.
2. **Debugging Tools**:
- Videos and HAR traces are recorded in the `test-results` directory.
@@ -137,11 +118,23 @@ To execute the end-to-end authentication tests, run the following commands from
- `debug_after_login.png`: Screenshot post-login.
- `debug_page.html`: Captured HTML structure of the page.
4. **Server Logs**:
4. **Manual Debugging**:
```python
# Screenshot
page.screenshot(path="debug_after_login.png")
# HTML content
with open("debug_page.html", "w", encoding="utf-8") as f:
f.write(page.content())
```
5. **Server Logs**:
When using the `app_server` fixture, server `stdout` and `stderr` are recorded during the test execution.
---
## Test Scenarios
The main authentication tests cover the following scenarios:
@@ -168,20 +161,17 @@ For a full list of test cases, see `tests/test_e2e_authentication.py`.
## Troubleshooting
- **Server Not Starting**:
If the server fails to start during testing, confirm the `DB_PATH`, `BASE_URL`, and all dependencies are correctly configured in the `.env` file.
If the server fails to start during testing, confirm the `DB_PATH`, `BASE_URL`, and all dependencies are correctly
configured in the `.env` file.
- **Playwright Issues**:
Ensure Playwright dependencies (`playwright install`) are installed and functional. Missing browsers or outdated Playwright versions can cause tests to fail.
Ensure Playwright dependencies (`playwright install`) are installed and functional. Missing browsers or outdated
Playwright versions can cause tests to fail.
- **Database Error**:
Check that the `tools.db` file or the temporary test database exists and is accessible. Ensure the test database environment variables are set correctly during tests.
Check that the `tools.db` file or the temporary test database exists and is accessible. Ensure the test database
environment variables are set correctly during tests.
---
## Additional Notes
- The app server is started with test-specific configurations (`app_server` fixture) that ensure end-to-end isolation.
- Tests requiring authentication use an admin user defined in the `test_users` fixture:
- Email: `test.admin@test.com`
- Password: `TestAdmin123`
- Ensure no sensitive data or configurations are hardcoded in the test scripts.
igurations are hardcoded in the test scripts.

View File

@@ -0,0 +1,4 @@
class Login:
email = ["input[type='email']", "input[name='email']"]
password = ["input[type='password']", "input[name='password']"]
submit = "button[type='submit']"

94
tests/e2e/e2e_helper.py Normal file
View File

@@ -0,0 +1,94 @@
from playwright.sync_api import Page, expect
def locate(page: Page, name, check_visible=True):
"""
Locate an element or elements on the provided page by name.
This function identifies elements on a web page using the locator API of the
provided page object. The `name` parameter can either be a single locator
string or a list of locator strings. If a list is provided, its elements are
joined into a single comma-separated string before locating the elements.
Args:
page (Page): The web page object in which to locate the element(s).
name (Union[str, List[str]]): Locator or list of locators to identify elements.
check_visible(bool): Whether to check if the element is visible before returning it.
Returns:
Locator: The locator object for the given name.
"""
if isinstance(name, list):
name = ",".join(name)
element = page.locator(name)
if check_visible:
expect(element).to_be_visible()
return element
def fill(element, value):
"""
Fills an element with a specified value and verifies that the element's value matches the provided value.
Parameters:
element : The UI element to be filled with the value.
value : The value to fill into the element.
Raises:
AssertionError: If the element's value does not match the provided value after the fill operation.
"""
element.fill(value)
expect(element).to_have_value(value)
def debug(page: Page, checkpoint_name: str):
"""
Takes a screenshot of the current state of a page and saves its full HTML content
to support debugging at a specified checkpoint. The screenshot and HTML file are saved
with checkpoint-specific filenames in the current working directory.
This function is useful for capturing the visual state and content of the page at
any moment during the script execution, helping in resolving issues or verifying
state transitions.
Arguments:
page (Page): The page object whose state and content are to be captured.
checkpoint_name (str): A descriptive name for the checkpoint. This is used to
generate unique filenames for the screenshot and HTML file.
"""
# Take screenshot
page.screenshot(path=f"debug_{checkpoint_name}.png")
# Save full HTML for inspection
with open(f"debug_{checkpoint_name}.html", "w", encoding="utf-8") as f:
f.write(page.content())
with open(f"debug_{checkpoint_name}.txt", "w", encoding="utf-8") as f:
f.writelines([
f"Checkpoint: {checkpoint_name}\n",
f"URL: {page.url}\n",
f"Title: {page.title()}\n",
])
def check_not_in_content(page, keywords):
"""
Checks that none of the specified keywords are present in the page content.
This function iterates through a list of keywords and verifies that none of
them are present in the given page's content. If any keyword is found, an
AssertionError is raised with details about the number of times it was found.
Args:
page: A page object that provides access to retrieve text content.
keywords: List of strings representing keywords to search in the page content.
Raises:
AssertionError: If any of the keywords are found in the page content.
"""
for keyword in keywords:
occurrences = page.get_by_text(keyword, exact=False)
assert occurrences.count() == 0, f"Found {occurrences.count()} of '{keyword}' in page content which should not be visible"

View File

@@ -3,6 +3,8 @@
import pytest
from playwright.sync_api import Page, expect
from e2e.e2e_constants import Login
from e2e.e2e_helper import locate, fill, debug, check_not_in_content
from playwright_config import BASE_URL
@@ -149,63 +151,18 @@ class TestAuthentication:
expect(page).to_have_url(f"{BASE_URL}/authlogin/login")
# Step 2: Locate form elements
email_input = page.locator("input[type='email'], input[name='email']")
password_input = page.locator("input[type='password'], input[name='password']")
submit_button = page.locator("button[type='submit']")
email_input = locate(page, Login.email)
password_input = locate(page, Login.password)
submit_button = locate(page, Login.submit)
# Verify all required elements are present and visible
expect(email_input).to_be_visible()
expect(password_input).to_be_visible()
expect(submit_button).to_be_visible()
fill(email_input, admin_user["email"])
fill(password_input, admin_user["password"])
# Step 3: Fill in valid credentials
email_input.fill(admin_user["email"])
password_input.fill(admin_user["password"])
# Verify credentials were entered correctly
expect(email_input).to_have_value(admin_user["email"])
expect(password_input).to_have_value(admin_user["password"])
# Step 4: Submit the login form
# Use click with wait for navigation to handle the redirect
with page.expect_navigation(wait_until="networkidle"):
submit_button.click()
# DEBUGGING BLOCK - Add this
print(f"🔍 Current URL: {page.url}")
print(f"🔍 Page title: {page.title()}")
# Take screenshot
page.screenshot(path="debug_after_login.png")
# Save full HTML for inspection
with open("debug_page.html", "w", encoding="utf-8") as f:
f.write(page.content())
# Check specific content that's causing the failure
page_content = page.content().lower()
login_keywords = ["sign in", "login", "authenticate"]
print("🔍 Checking for login keywords:")
for keyword in login_keywords:
if keyword in page_content:
print(f" ❌ Found '{keyword}' in page content")
# Find exactly where this keyword appears
occurrences = page.get_by_text(keyword, exact=False)
count = occurrences.count()
print(f" Found in {count} elements:")
for i in range(min(count, 3)): # Show first 3 occurrences
element = occurrences.nth(i)
try:
print(f" - Element {i}: '{element.inner_text()[:50]}...' (visible: {element.is_visible()})")
except:
print(f" - Element {i}: Could not get text")
else:
print(f"'{keyword}' NOT found")
# Your original assertion (will still fail, but now you have debug info)
has_login_content = any(keyword in page_content for keyword in login_keywords)
assert not has_login_content, "Login content still visible after successful authentication"
debug(page, "after_login")
check_not_in_content(page, ["sign in", "login", "authenticate"])
page.pause()