Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 84 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,47 +151,96 @@ Pre-commit hooks run automatically on `git commit` and ensure consistent code qu
## 🏗️ Project Structure

```
playwright-python-async-template/
├── pages/ # Page Object Model
│ ├── base_page.py # Core browser interactions
│ ├── standard_web_page.py # Common UI patterns (CRUD, filters, etc.)
│ ├── login_page.py # Authentication
│ └── examples/ # Example page objects
playwright-python-async-framework/
├── core/ # Framework internals
│ ├── api/ # API testing layer
│ │ ├── base_client.py # BaseAPIClient (HTTP methods + auth)
│ │ ├── http_client.py # Low-level HTTP with retries
│ │ ├── config.py # HTTPConfig presets
│ │ ├── models.py # API exception models
│ │ ├── services/
│ │ │ ├── auth/ # Authentication strategies
│ │ │ │ └── strategies/ # Bearer, APIKey, Basic, OAuth2, etc.
│ │ │ ├── response/ # APIResponseWrapper
│ │ │ ├── retry.py # Retry with exponential backoff
│ │ │ ├── interceptor.py # Request/response interceptors
│ │ │ └── validation.py # Schema + status code validation
│ │ ├── README.md # API usage guide (user-facing)
│ │ └── DEVELOPER_GUIDE.md # How to extend the API layer
│ │
│ ├── ui/ # UI testing layer
│ │ ├── base_page.py # BasePage with lazy-loaded services
│ │ ├── ai/ # AI-powered selector healing
│ │ │ ├── locator_healer.py # Main healing orchestrator
│ │ │ ├── cache_manager.py # Persistent healing cache
│ │ │ ├── metrics_tracker.py # Healing metrics & reports
│ │ │ └── extraction/ # DOM extraction strategies
│ │ ├── browser/
│ │ │ ├── browser_manager.py # Browser lifecycle management
│ │ │ └── strategies/ # Local / CI / Debug strategies
│ │ ├── components/ # Reusable UI components
│ │ │ ├── button.py, checkbox.py, datepicker.py
│ │ │ ├── input.py, modal.py, radio.py
│ │ │ ├── select.py, select2.py, table.py
│ │ │ └── file.py
│ │ ├── services/ # Page interaction services
│ │ │ ├── attribute.py # DOM attribute manipulation
│ │ │ ├── screenshot.py # Evidence capture
│ │ │ ├── storage.py # LocalStorage / Cookies
│ │ │ ├── tab_window.py # Multi-tab management
│ │ │ ├── validation.py # Assertion helpers
│ │ │ ├── wait.py # Page-load waiting
│ │ │ └── form/ # Form-filling strategies
│ │ ├── wrappers/ # Locator wrappers
│ │ │ ├── retry_locator.py # Retry failed operations
│ │ │ └── smart_locator.py # AI healing + retry wrapper
│ │ ├── README.md # UI usage guide (user-facing)
│ │ └── DEVELOPER_GUIDE.md # How to extend the UI layer
│ │
│ ├── reporting/
│ │ └── pytest_hooks.py # Auto-screenshot on failure + Allure
│ │
│ └── utils/
│ ├── exceptions.py # Custom exception types
│ ├── logger_config.py # Centralized logging setup
│ └── playwright_utils.py # Locator resolution helpers
├── helpers/ # Helper modules
│ ├── api_client.py # API testing client
│ ├── database.py # Database client (PostgreSQL, MySQL, etc.)
│ └── redis_client.py # Redis client
├── services/ # Project-level page objects
│ └── base_pages/
│ └── login_page.py # Generic login page (used by conftest)
├── utils/ # Utilities
│ ├── config.py # Configuration management
│ ├── consts.py # Constants and enums
│ ├── exceptions.py # Custom exceptions
│ └── test_helpers.py # Test utilities
├── helpers/ # Infrastructure clients
│ ├── database.py # DatabaseClient (PostgreSQL, MySQL, …)
│ └── redis_client.py # RedisClient
├── tests/ # Test suites
│ ├── test_ui_examples.py # UI testing examples
│ ├── test_api_examples.py # API testing examples
│ ├── test_database_examples.py # Database testing examples
│ └── test_crud_example.py # Complete CRUD example
├── utils/ # Project-level utilities
│ ├── config.py # Centralized configuration (env vars)
│ ├── consts.py # Enumerations (FilterType, etc.)
│ └── test_helpers.py # TestDataGenerator + TestHelpers
├── docs/ # Documentation
│ ├── UI_TESTING.md # UI testing guide
│ ├── API_TESTING.md # API testing guide
│ └── DATABASE_TESTING.md # Database testing guide
├── tests/ # Example test suites
│ ├── test_ui_examples.py # UI automation examples
│ ├── test_api_examples.py # REST API testing examples
│ ├── test_database_examples.py # Database & Redis testing examples
│ └── test_crud_example.py # Complete CRUD workflow example
├── ci/ # CI/CD configuration
│ └── Jenkinsfile # Jenkins pipeline
├── docs/ # User-facing documentation
│ ├── UI_TESTING.md # UI testing patterns & examples
│ ├── API_TESTING.md # API testing patterns & examples
│ └── DATABASE_TESTING.md # Database testing patterns & examples
├── conftest.py # Pytest configuration and fixtures
├── pytest.ini # Pytest settings
├── requirements.txt # Python dependencies
├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose orchestration
├── .pre-commit-config.yaml # Pre-commit hooks configuration
├── pyproject.toml # Python tooling configuration
├── .env.example # Environment template
└── README.md # This file
├── ci/ # CI/CD configuration
│ └── Jenkinsfile # Jenkins pipeline
├── conftest.py # Pytest fixtures (browser, page, api_client, …)
├── pytest.ini # Pytest settings (markers, testpaths, …)
├── requirements.txt # Python dependencies
├── Dockerfile # Docker image for test runner
├── docker-compose.yml # Multi-service Docker Compose (DB + Redis)
├── .pre-commit-config.yaml # Pre-commit hooks (black, flake8, mypy, …)
├── pyproject.toml # Tool configuration (black, isort, mypy, …)
├── .env.example # Environment variable template
└── README.md # This file
```

---
Expand Down
27 changes: 27 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from core.utils.logger_config import configure_logging
from helpers.database import DatabaseClient

from core.api.base_client import BaseAPIClient

# Custom imports
from helpers.redis_client import RedisClient
from services.base_pages.login_page import LoginPage
Expand Down Expand Up @@ -162,6 +164,31 @@ async def _connect_to_environment(username: str, password: str, url: str):
return _connect_to_environment


# --- API client fixture -------------------------------------------------------
@pytest_asyncio.fixture(scope="function")
async def api_client(context: BrowserContext) -> BaseAPIClient:
"""
Provides a ready-to-use BaseAPIClient for API testing.

The client is pre-configured with the base URL from the
``API_BASE_URL`` environment variable (falls back to ``BASE_URL``).
Authentication can be configured per-test via:

- ``client.set_bearer_token("token")``
- ``client.set_api_key("key")``
- ``client.set_basic_auth("user", "pass")``

Example::

async def test_get_users(api_client):
api_client.set_bearer_token(Config.get_api_bearer_token())
response = await api_client.get("/users")
assert response.is_success
assert isinstance(response.data, list)
"""
return BaseAPIClient(context.request, Config.get_api_base_url())


# --- Database fixtures --------------------------------------------------------
@pytest_asyncio.fixture
async def db_client():
Expand Down
68 changes: 47 additions & 21 deletions core/api/services/auth/strategies/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,31 +70,57 @@ def _needs_refresh(self) -> bool:

async def _fetch_token(self) -> None:
"""
Fetch access token from token endpoint.
Fetch access token from token endpoint using client credentials flow.

Note: Requires HTTPClient to be injected or uses requests library.
Makes a POST request to the token URL with client credentials.
Uses the standard OAuth2 client_credentials grant type.

Raises:
RuntimeError: If token request fails or response is invalid
"""
# This would use HTTPClient in real implementation
# For now, showing the structure
import asyncio
import json
import urllib.error
import urllib.parse
import urllib.request

logger.info(f"Fetching OAuth2 token from {self.token_url}")

# In real implementation:
# response = await http_client.post(
# self.token_url,
# data={
# 'grant_type': 'client_credentials',
# 'client_id': self.client_id,
# 'client_secret': self.client_secret,
# 'scope': self.scope
# }
# )
# self._access_token = response.data['access_token']
# self._expires_at = time.time() + response.data.get('expires_in', 3600)

raise NotImplementedError(
"OAuth2 token fetching requires HTTPClient integration. "
"Use BearerTokenAuth with pre-fetched token for now."
)
post_data: dict = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
}
if self.scope:
post_data["scope"] = self.scope

encoded_data = urllib.parse.urlencode(post_data).encode("utf-8")

def _sync_fetch() -> dict:
req = urllib.request.Request(
self.token_url,
data=encoded_data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
method="POST",
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode("utf-8"))

try:
token_response = await asyncio.to_thread(_sync_fetch)
self._access_token = token_response["access_token"]
expires_in = token_response.get("expires_in", 3600)
self._expires_at = time.time() + int(expires_in)
logger.info("OAuth2 token fetched successfully")
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8")
raise RuntimeError(
f"OAuth2 token request failed (HTTP {e.code}): {error_body}"
) from e
except (KeyError, ValueError) as e:
raise RuntimeError(f"Invalid OAuth2 token response: {e}") from e
except Exception as e:
raise RuntimeError(f"Failed to fetch OAuth2 token: {e}") from e

@classmethod
def from_env(
Expand Down
Loading