refactoring integration tests
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ tests/TestDBEngineRoot
|
|||||||
tests/*.png
|
tests/*.png
|
||||||
src/*.png
|
src/*.png
|
||||||
tests/*.html
|
tests/*.html
|
||||||
|
tests/*.txt
|
||||||
test-results
|
test-results
|
||||||
.sesskey
|
.sesskey
|
||||||
tools.db
|
tools.db
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -49,6 +49,9 @@ clean:
|
|||||||
rm -rf src/tools.db
|
rm -rf src/tools.db
|
||||||
rm -rf src/*.out
|
rm -rf src/*.out
|
||||||
rm -rf src/*.prof
|
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 '.sesskey' -exec rm -rf {} +
|
||||||
find . -name '.pytest_cache' -exec rm -rf {} +
|
find . -name '.pytest_cache' -exec rm -rf {} +
|
||||||
find . -name '__pycache__' -exec rm -rf {} +
|
find . -name '__pycache__' -exec rm -rf {} +
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ load_dotenv()
|
|||||||
DB_PATH = os.getenv("DB_PATH", "tools.db")
|
DB_PATH = os.getenv("DB_PATH", "tools.db")
|
||||||
logger.info(f"{DB_PATH=}")
|
logger.info(f"{DB_PATH=}")
|
||||||
|
|
||||||
|
# Custom database engine settings
|
||||||
|
DBENGINE_PATH = os.getenv("DBENGINE_PATH", ".mytools_db")
|
||||||
|
logger.info(f"{DBENGINE_PATH=}")
|
||||||
|
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
|
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import config
|
||||||
from constants import NOT_LOGGED, NO_SESSION
|
from constants import NOT_LOGGED, NO_SESSION
|
||||||
from core.dbengine import DbEngine, TAG_PARENT, TAG_USER, TAG_DATE, DbException
|
from core.dbengine import DbEngine, TAG_PARENT, TAG_USER, TAG_DATE, DbException
|
||||||
from core.settings_objects import *
|
from core.settings_objects import *
|
||||||
@@ -92,7 +93,7 @@ class MemoryDbEngine:
|
|||||||
|
|
||||||
class SettingsManager:
|
class SettingsManager:
|
||||||
def __init__(self, engine=None):
|
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):
|
def save(self, session: dict, entry: str, obj: object):
|
||||||
user_id, user_email = self._get_user(session)
|
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}'.")
|
raise AttributeError(f"Settings '{self._obj_entry}' has no attribute '{self._obj_attribute}'.")
|
||||||
|
|
||||||
return settings, getattr(settings, 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
|
||||||
|
|||||||
19
src/main.py
19
src/main.py
@@ -20,10 +20,10 @@ from components.register.constants import ROUTE_ROOT as REGISTER_ROUTE_ROOT
|
|||||||
from components.register.constants import Routes as RegisterRoutes
|
from components.register.constants import Routes as RegisterRoutes
|
||||||
from config import APP_PORT
|
from config import APP_PORT
|
||||||
from constants import Routes
|
from constants import Routes
|
||||||
from core.dbengine import DbException
|
|
||||||
from core.database_manager import initialize_database
|
from core.database_manager import initialize_database
|
||||||
|
from core.dbengine import DbException
|
||||||
from core.instance_manager import InstanceManager
|
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.admin_import_settings import AdminImportSettings, IMPORT_SETTINGS_PATH, import_settings_app
|
||||||
from pages.another_grid import get_datagrid2
|
from pages.another_grid import get_datagrid2
|
||||||
from pages.basic_test import BASIC_TEST_PATH, basic_test_app, get_basic_test
|
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
|
return response
|
||||||
|
|
||||||
|
|
||||||
settings_manager = SettingsManager()
|
import_settings = AdminImportSettings(get_settings_manager(), None)
|
||||||
|
|
||||||
import_settings = AdminImportSettings(settings_manager, None)
|
|
||||||
pages = [
|
pages = [
|
||||||
Page("My table", get_datagrid, id="my_table"),
|
Page("My table", get_datagrid, id="my_table"),
|
||||||
Page("new settings", import_settings, id="import_settings"),
|
Page("new settings", import_settings, id="import_settings"),
|
||||||
@@ -247,8 +245,8 @@ pages = [
|
|||||||
Page("Another Table", get_datagrid2, id="another_table"),
|
Page("Another Table", get_datagrid2, id="another_table"),
|
||||||
]
|
]
|
||||||
|
|
||||||
login = Login(settings_manager)
|
login = Login(get_settings_manager())
|
||||||
register = Register(settings_manager)
|
register = Register(get_settings_manager())
|
||||||
InstanceManager.register_many(login, register)
|
InstanceManager.register_many(login, register)
|
||||||
|
|
||||||
|
|
||||||
@@ -258,8 +256,8 @@ def get(session):
|
|||||||
main = InstanceManager.get(session,
|
main = InstanceManager.get(session,
|
||||||
DrawerLayout.create_component_id(session),
|
DrawerLayout.create_component_id(session),
|
||||||
DrawerLayout,
|
DrawerLayout,
|
||||||
settings_manager=settings_manager)
|
settings_manager=get_settings_manager())
|
||||||
return page_layout_lite(session, settings_manager, main)
|
return page_layout_lite(session, get_settings_manager(), main)
|
||||||
except DbException:
|
except DbException:
|
||||||
return RedirectResponse(LOGIN_ROUTE_ROOT + LoginRoutes.Logout, status_code=303)
|
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(
|
return page_layout_new(
|
||||||
session=session,
|
session=session,
|
||||||
settings_manager=settings_manager,
|
settings_manager=get_settings_manager(),
|
||||||
content=error_content
|
content=error_content
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -317,6 +315,7 @@ def main(port):
|
|||||||
logger.info(f" Starting FastHTML server on http://localhost:{port}")
|
logger.info(f" Starting FastHTML server on http://localhost:{port}")
|
||||||
serve(port=port)
|
serve(port=port)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Start your application
|
# Start your application
|
||||||
logger.info("Application starting...")
|
logger.info("Application starting...")
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from io import BytesIO
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
import requests
|
import requests
|
||||||
import time
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from components.datagrid.DataGrid import reset_instances
|
from components.datagrid.DataGrid import reset_instances
|
||||||
from playwright_config import BROWSER_CONFIG, BASE_URL
|
from playwright_config import BROWSER_CONFIG, BASE_URL
|
||||||
@@ -18,110 +20,124 @@ USER_EMAIL = "test@mail.com"
|
|||||||
USER_ID = "test_user"
|
USER_ID = "test_user"
|
||||||
APP_PORT = 5002
|
APP_PORT = 5002
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def test_database():
|
def test_database():
|
||||||
"""Configure temporary database for tests"""
|
"""Configure temporary database for tests"""
|
||||||
# Create temporary DB
|
# Create temporary DB
|
||||||
temp_db_path = TestDatabaseManager.create_temp_db_path()
|
temp_db_path = TestDatabaseManager.create_temp_db_path()
|
||||||
|
|
||||||
# Save original environment
|
# Save original environment
|
||||||
original_env = {
|
original_env = {
|
||||||
"DB_PATH": os.environ.get("DB_PATH"),
|
"DB_PATH": os.environ.get("DB_PATH"),
|
||||||
"ADMIN_EMAIL": os.environ.get("ADMIN_EMAIL"),
|
"ADMIN_EMAIL": os.environ.get("ADMIN_EMAIL"),
|
||||||
"ADMIN_PASSWORD": os.environ.get("ADMIN_PASSWORD"),
|
"ADMIN_PASSWORD": os.environ.get("ADMIN_PASSWORD"),
|
||||||
"SECRET_KEY": os.environ.get("SECRET_KEY")
|
"SECRET_KEY": os.environ.get("SECRET_KEY")
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configure test environment
|
# Configure test environment
|
||||||
TestDatabaseManager.setup_test_environment(temp_db_path)
|
TestDatabaseManager.setup_test_environment(temp_db_path)
|
||||||
|
|
||||||
print(f"Test database created at: {temp_db_path}")
|
print(f"Test database created at: {temp_db_path}")
|
||||||
|
|
||||||
yield temp_db_path
|
yield temp_db_path
|
||||||
|
|
||||||
# Cleanup: restore environment and clean up DB
|
# Cleanup: restore environment and clean up DB
|
||||||
TestDatabaseManager.restore_original_environment(original_env)
|
TestDatabaseManager.restore_original_environment(original_env)
|
||||||
TestDatabaseManager.cleanup_test_db(temp_db_path)
|
TestDatabaseManager.cleanup_test_db(temp_db_path)
|
||||||
print(f"Test database cleaned up: {temp_db_path}")
|
print(f"Test database cleaned up: {temp_db_path}")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def test_users():
|
|
||||||
"""Define available test users"""
|
|
||||||
return {
|
|
||||||
"admin": TestUsers.ADMIN,
|
|
||||||
"regular_user": TestUsers.REGULAR_USER,
|
|
||||||
"invalid_cases": TestUsers.INVALID_CREDENTIALS,
|
|
||||||
"protected_urls": TestUsers.PROTECTED_URLS
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
def app_server(test_database, test_users):
|
def test_users():
|
||||||
"""Start application server with test database"""
|
"""Define available test users"""
|
||||||
# Use the same Python executable that's running pytest
|
return {
|
||||||
python_executable = sys.executable
|
"admin": TestUsers.ADMIN,
|
||||||
|
"regular_user": TestUsers.REGULAR_USER,
|
||||||
# Get the absolute path to the src directory
|
"invalid_cases": TestUsers.INVALID_CREDENTIALS,
|
||||||
project_root = Path(__file__).parent.parent
|
"protected_urls": TestUsers.PROTECTED_URLS
|
||||||
src_path = project_root / "src"
|
}
|
||||||
|
|
||||||
# Create test environment
|
|
||||||
test_env = os.environ.copy()
|
@pytest.fixture(scope="session")
|
||||||
test_env["DB_PATH"] = test_database
|
def test_setting_manager():
|
||||||
test_env["ADMIN_EMAIL"] = test_users["admin"]["email"]
|
# create a dedicated folder for the db
|
||||||
test_env["ADMIN_PASSWORD"] = test_users["admin"]["password"]
|
db_engine_folder = tempfile.mkdtemp(prefix="test_db_engine")
|
||||||
test_env["SECRET_KEY"] = "test-secret-key-for-e2e-tests"
|
|
||||||
|
yield db_engine_folder
|
||||||
# Start the application server
|
|
||||||
print(f"Starting server on url {BASE_URL} with test database...")
|
# clean up folder & restore settings manager
|
||||||
port = BASE_URL.split(':')[-1].split('/')[0] if ':' in BASE_URL else APP_PORT
|
if os.path.exists(db_engine_folder):
|
||||||
print(f"Using port {port}")
|
shutil.rmtree(db_engine_folder, ignore_errors=True)
|
||||||
print(f"Test DB path: {test_database}")
|
|
||||||
|
|
||||||
server_process = subprocess.Popen(
|
@pytest.fixture(scope="session")
|
||||||
[python_executable, "main.py", "--port", "5002"],
|
def app_server(test_database, test_users, test_setting_manager):
|
||||||
cwd=str(src_path), # Change to src directory where main.py is located
|
"""Start application server with test database"""
|
||||||
stdout=subprocess.PIPE,
|
# Use the same Python executable that's running pytest
|
||||||
stderr=subprocess.PIPE,
|
python_executable = sys.executable
|
||||||
env=test_env # Use test environment
|
|
||||||
)
|
# Get the absolute path to the src directory
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
# Wait for the server to start
|
src_path = project_root / "src"
|
||||||
max_retries = 10 # Wait up to 30 seconds
|
|
||||||
for i in range(max_retries):
|
# Create test environment
|
||||||
try:
|
test_env = os.environ.copy()
|
||||||
print(f"Waiting retry {i}/{max_retries}")
|
test_env["DB_PATH"] = test_database
|
||||||
response = requests.get(BASE_URL, timeout=1)
|
test_env["DBENGINE_PATH"] = test_setting_manager
|
||||||
if response.status_code in [200, 302, 404]: # Server is responding
|
test_env["ADMIN_EMAIL"] = test_users["admin"]["email"]
|
||||||
print(f"Server started successfully after {i + 1} attempts")
|
test_env["ADMIN_PASSWORD"] = test_users["admin"]["password"]
|
||||||
break
|
test_env["SECRET_KEY"] = "test-secret-key-for-e2e-tests"
|
||||||
except requests.exceptions.RequestException:
|
|
||||||
time.sleep(1)
|
# Start the application server
|
||||||
else:
|
print(f"Starting server on url {BASE_URL} with test database...")
|
||||||
# If we get here, the server didn't start in time
|
port = BASE_URL.split(':')[-1].split('/')[0] if ':' in BASE_URL else APP_PORT
|
||||||
print(f"Failed to start after {max_retries} attempts.")
|
print(f"Using port {port}")
|
||||||
server_process.kill()
|
print(f"Test DB path: {test_database}")
|
||||||
stdout, stderr = server_process.communicate()
|
|
||||||
raise RuntimeError(
|
server_process = subprocess.Popen(
|
||||||
f"Server failed to start within {max_retries} seconds.\n"
|
[python_executable, "main.py", "--port", "5002"],
|
||||||
f"STDOUT: {stdout.decode()}\n"
|
cwd=str(src_path), # Change to src directory where main.py is located
|
||||||
f"STDERR: {stderr.decode()}"
|
stdout=subprocess.PIPE,
|
||||||
)
|
stderr=subprocess.PIPE,
|
||||||
|
env=test_env # Use test environment
|
||||||
# Yield control to the tests
|
)
|
||||||
print('Test server started with isolated database!')
|
|
||||||
yield server_process
|
# Wait for the server to start
|
||||||
|
max_retries = 10 # Wait up to 30 seconds
|
||||||
# Cleanup: terminate the server after tests
|
for i in range(max_retries):
|
||||||
server_process.terminate()
|
|
||||||
try:
|
try:
|
||||||
server_process.wait(timeout=5)
|
print(f"Waiting retry {i}/{max_retries}")
|
||||||
print('Test server stopped.')
|
response = requests.get(BASE_URL, timeout=1)
|
||||||
except subprocess.TimeoutExpired:
|
if response.status_code in [200, 302, 404]: # Server is responding
|
||||||
server_process.kill()
|
print(f"Server started successfully after {i + 1} attempts")
|
||||||
server_process.wait()
|
break
|
||||||
print('Test server killed!')
|
except requests.exceptions.RequestException:
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
# If we get here, the server didn't start in time
|
||||||
|
print(f"Failed to start after {max_retries} attempts.")
|
||||||
|
server_process.kill()
|
||||||
|
stdout, stderr = server_process.communicate()
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Server failed to start within {max_retries} seconds.\n"
|
||||||
|
f"STDOUT: {stdout.decode()}\n"
|
||||||
|
f"STDERR: {stderr.decode()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Yield control to the tests
|
||||||
|
print('Test server started with isolated database!')
|
||||||
|
yield server_process
|
||||||
|
|
||||||
|
# Cleanup: terminate the server after tests
|
||||||
|
server_process.terminate()
|
||||||
|
try:
|
||||||
|
server_process.wait(timeout=5)
|
||||||
|
print('Test server stopped.')
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
server_process.kill()
|
||||||
|
server_process.wait()
|
||||||
|
print('Test server killed!')
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
# README - End-to-End Authentication Testing Guide
|
# 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
|
## 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Setup Instructions](#setup-instructions)
|
1. [Setup Instructions](#setup-instructions)
|
||||||
- [Application Setup](#application-setup)
|
- [Application Setup](#application-setup)
|
||||||
- [Database Setup](#database-setup)
|
- [Database Setup](#database-setup)
|
||||||
2. [Installing Dependencies](#installing-dependencies)
|
2. [Installing Dependencies](#installing-dependencies)
|
||||||
3. [Running Tests](#running-tests)
|
3. [Running Tests](#running-tests)
|
||||||
4. [Debugging and Logs](#debugging-and-logs)
|
4. [Debugging and Logs](#debugging-and-logs)
|
||||||
@@ -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
|
## Installing Dependencies
|
||||||
|
|
||||||
Install the required packages for the tests:
|
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
|
## Running Tests
|
||||||
|
|
||||||
To execute the end-to-end authentication tests, run the following commands from the project root.
|
To execute the end-to-end authentication tests, run the following commands from the project root.
|
||||||
@@ -126,22 +106,35 @@ To execute the end-to-end authentication tests, run the following commands from
|
|||||||
## Debugging and Logs
|
## Debugging and Logs
|
||||||
|
|
||||||
1. **Console 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**:
|
2. **Debugging Tools**:
|
||||||
- Videos and HAR traces are recorded in the `test-results` directory.
|
- Videos and HAR traces are recorded in the `test-results` directory.
|
||||||
- Use the `page.pause()` method inside a test for interactive debugging with Playwright Inspector.
|
- Use the `page.pause()` method inside a test for interactive debugging with Playwright Inspector.
|
||||||
|
|
||||||
3. **Screenshots and Debug HTML**:
|
3. **Screenshots and Debug HTML**:
|
||||||
If a test fails, screenshots and full HTML are saved for post-mortem debugging:
|
If a test fails, screenshots and full HTML are saved for post-mortem debugging:
|
||||||
- `debug_after_login.png`: Screenshot post-login.
|
- `debug_after_login.png`: Screenshot post-login.
|
||||||
- `debug_page.html`: Captured HTML structure of the page.
|
- `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.
|
When using the `app_server` fixture, server `stdout` and `stderr` are recorded during the test execution.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Test Scenarios
|
## Test Scenarios
|
||||||
|
|
||||||
The main authentication tests cover the following 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
|
## Troubleshooting
|
||||||
|
|
||||||
- **Server Not Starting**:
|
- **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**:
|
- **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**:
|
- **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
|
igurations are hardcoded in the test scripts.
|
||||||
|
|
||||||
- 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.
|
|
||||||
4
tests/e2e/e2e_constants.py
Normal file
4
tests/e2e/e2e_constants.py
Normal 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
94
tests/e2e/e2e_helper.py
Normal 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"
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from playwright.sync_api import Page, expect
|
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
|
from playwright_config import BASE_URL
|
||||||
|
|
||||||
|
|
||||||
@@ -149,63 +151,18 @@ class TestAuthentication:
|
|||||||
expect(page).to_have_url(f"{BASE_URL}/authlogin/login")
|
expect(page).to_have_url(f"{BASE_URL}/authlogin/login")
|
||||||
|
|
||||||
# Step 2: Locate form elements
|
# Step 2: Locate form elements
|
||||||
email_input = page.locator("input[type='email'], input[name='email']")
|
email_input = locate(page, Login.email)
|
||||||
password_input = page.locator("input[type='password'], input[name='password']")
|
password_input = locate(page, Login.password)
|
||||||
submit_button = page.locator("button[type='submit']")
|
submit_button = locate(page, Login.submit)
|
||||||
|
|
||||||
# Verify all required elements are present and visible
|
fill(email_input, admin_user["email"])
|
||||||
expect(email_input).to_be_visible()
|
fill(password_input, admin_user["password"])
|
||||||
expect(password_input).to_be_visible()
|
|
||||||
expect(submit_button).to_be_visible()
|
|
||||||
|
|
||||||
# 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"):
|
with page.expect_navigation(wait_until="networkidle"):
|
||||||
submit_button.click()
|
submit_button.click()
|
||||||
|
|
||||||
# DEBUGGING BLOCK - Add this
|
debug(page, "after_login")
|
||||||
print(f"🔍 Current URL: {page.url}")
|
check_not_in_content(page, ["sign in", "login", "authenticate"])
|
||||||
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"
|
|
||||||
|
|
||||||
page.pause()
|
page.pause()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user