Testing Guide¶
This guide covers the testing approach for the Evrmore Authentication system, including unit tests, integration tests, and how to run and extend the test suite.
Testing Philosophy¶
The Evrmore Authentication system follows these testing principles:
- Test-Driven Development: Critical components are developed using TDD where possible.
- Comprehensive Coverage: Aim for high test coverage, especially for security-critical components.
- Isolation: Unit tests should be isolated and not depend on external services.
- Realistic Scenarios: Integration tests should simulate real-world usage patterns.
Test Structure¶
The tests are organized in the following structure:
evrmore_authentication/tests/
├── unit/ # Unit tests
│ ├── test_auth.py # Tests for core authentication functionality
│ ├── test_crypto.py # Tests for cryptographic functions
│ ├── test_models.py # Tests for data models
│ └── test_utils.py # Tests for utility functions
├── integration/ # Integration tests
│ ├── test_api.py # Tests for API endpoints
│ └── test_oauth.py # Tests for OAuth functionality
└── conftest.py # Pytest fixtures and configuration
Running Tests¶
Prerequisites¶
Before running tests, ensure you have the required dependencies:
Running All Tests¶
To run all tests:
Running Specific Test Categories¶
To run only unit tests:
To run only integration tests:
Running a Specific Test File¶
To run tests from a specific file:
Running a Specific Test¶
To run a specific test function:
Test Coverage¶
To generate a test coverage report:
For a detailed HTML coverage report:
This will create a htmlcov directory with an HTML report that you can open in your browser.
Writing Tests¶
Unit Tests¶
Unit tests should focus on testing individual functions or methods in isolation. Use mocks to avoid dependencies on external services or other components.
Example of a unit test:
def test_verify_signature(mocker):
# Arrange
mock_verify = mocker.patch('evrmore_authentication.crypto.verify_message', return_value=True)
auth = EvrmoreAuth()
address = "EXaMPLeEvRMoReAddResS"
message = "Test message"
signature = "TestSignature"
# Act
result = auth.verify_signature(address, message, signature)
# Assert
assert result is True
mock_verify.assert_called_once_with(address, message, signature)
Integration Tests¶
Integration tests should test the interaction between multiple components or the system as a whole. These tests may require more setup but provide confidence that the system works correctly in real-world scenarios.
Example of an integration test:
def test_authentication_flow():
# Arrange
auth = EvrmoreAuth()
address, private_key = create_wallet()
# Act
# 1. Generate a challenge
challenge = auth.generate_challenge(address)
# 2. Sign the challenge
signature = sign_message(private_key, challenge)
# 3. Authenticate with the signature
session = auth.authenticate(address, challenge, signature)
# Assert
assert session is not None
assert session.user_id is not None
assert session.evrmore_address == address
assert session.token is not None
# 4. Validate the token
token_data = auth.validate_token(session.token)
assert token_data is not None
assert token_data.get('sub') == session.user_id
assert token_data.get('evr_address') == address
Test Fixtures¶
Pytest fixtures are used to set up test dependencies and can be defined in conftest.py or in individual test files.
Example of fixtures in conftest.py:
import pytest
from evrmore_authentication import EvrmoreAuth
from evrmore_authentication.crypto import create_wallet
@pytest.fixture
def auth():
"""Return an initialized EvrmoreAuth instance."""
return EvrmoreAuth(jwt_secret="test-secret", debug=True)
@pytest.fixture
def test_wallet():
"""Create a test wallet for testing."""
address, private_key = create_wallet()
return {"address": address, "private_key": private_key}
@pytest.fixture
def authenticated_user(auth, test_wallet):
"""Return an authenticated user session."""
address = test_wallet["address"]
private_key = test_wallet["private_key"]
# Generate a challenge
challenge = auth.generate_challenge(address)
# Sign the challenge
signature = sign_message(private_key, challenge)
# Authenticate
session = auth.authenticate(address, challenge, signature)
return {
"session": session,
"address": address,
"challenge": challenge,
"signature": signature
}
Mocking¶
The pytest-mock plugin provides a mocker fixture that can be used to create mock objects and patch functions.
Example of mocking:
def test_get_user_by_token(mocker):
# Arrange
mock_validate = mocker.patch('evrmore_authentication.auth.EvrmoreAuth.validate_token')
mock_validate.return_value = {"sub": "test-user-id", "evr_address": "test-address"}
mock_get_user = mocker.patch('evrmore_authentication.models.User.get_by_id')
mock_user = mocker.MagicMock()
mock_user.id = "test-user-id"
mock_get_user.return_value = mock_user
auth = EvrmoreAuth()
# Act
user = auth.get_user_by_token("test-token")
# Assert
assert user is mock_user
mock_validate.assert_called_once_with("test-token")
mock_get_user.assert_called_once_with("test-user-id")
Testing API Endpoints¶
API endpoints can be tested using FastAPI's TestClient.
Example of testing an API endpoint:
from fastapi.testclient import TestClient
from evrmore_authentication.api import app
client = TestClient(app)
def test_generate_challenge_endpoint():
# Arrange
test_address = "EXaMPLeEvRMoReAddResS"
# Act
response = client.post(
"/auth/challenge",
json={"evrmore_address": test_address}
)
# Assert
assert response.status_code == 200
data = response.json()
assert "challenge" in data
assert data["status"] == "success"
Testing OAuth Functionality¶
OAuth functionality requires more complex setup and interaction between client and server.
Example of testing OAuth authorization:
def test_oauth_authorization_flow(mocker):
# Mock the authentication
mock_auth = mocker.patch('evrmore_authentication.oauth.authenticate_user')
mock_auth.return_value = {"user_id": "test-user-id", "evrmore_address": "test-address"}
# Mock the client validation
mock_client = mocker.patch('evrmore_authentication.oauth.get_client_by_id')
mock_client.return_value = {
"id": "test-client-id",
"name": "Test Client",
"redirect_uris": ["http://localhost/callback"],
"scopes": ["profile"]
}
# Create a test client
client = TestClient(app)
# Act - Initiate the authorization flow
response = client.get(
"/oauth/auth",
params={
"client_id": "test-client-id",
"redirect_uri": "http://localhost/callback",
"response_type": "code",
"scope": "profile",
"state": "test-state"
}
)
# Assert - Should redirect to login page
assert response.status_code == 302
assert "/oauth/login" in response.headers["location"]
# Act - Simulate successful login and authorization
response = client.post(
"/oauth/login",
json={
"evrmore_address": "test-address",
"challenge": "test-challenge",
"signature": "test-signature",
"client_id": "test-client-id",
"redirect_uri": "http://localhost/callback",
"scope": "profile",
"state": "test-state"
}
)
# Assert - Should redirect to the client with an authorization code
assert response.status_code == 302
redirect_uri = response.headers["location"]
assert "http://localhost/callback" in redirect_uri
assert "code=" in redirect_uri
assert "state=test-state" in redirect_uri
Continuous Integration¶
Tests are automatically run in the CI pipeline on every push and pull request. The CI configuration is defined in the .github/workflows/tests.yml file.
Test Environment¶
Tests use an in-memory SQLite database by default to avoid affecting any production data. The database is created fresh for each test run and destroyed afterward.
To configure the test environment, you can set environment variables or modify the conftest.py file.
Troubleshooting Tests¶
Common Issues¶
- Database Errors: Ensure the test database is properly configured and accessible.
- Import Errors: Check that all required dependencies are installed.
- Mock Issues: Verify that mocks are correctly set up and returning the expected values.
- Fixture Errors: Ensure fixtures are correctly defined and dependencies are resolved.
Debugging Tests¶
To run tests with more verbose output:
To enable print statements during tests:
To debug a specific test with a debugger:
Best Practices¶
- Keep Tests Fast: Tests should run quickly to encourage frequent testing.
- Independent Tests: Tests should not depend on each other or run in a specific order.
- Clear Assertions: Make assertions clear and specific to what is being tested.
- Test Edge Cases: Include tests for edge cases and error conditions.
- Maintain Tests: Update tests when the code changes to prevent test debt.
Contributing Tests¶
When contributing new features or bug fixes, please include appropriate tests. Follow these guidelines:
- Write tests for new functionality before implementing it (TDD).
- Add regression tests for bug fixes to prevent the bug from recurring.
- Follow the existing test structure and naming conventions.
- Ensure all tests pass before submitting a pull request.