- Document decoupling approach for utils/auth.py - Remove all environment variable dependencies - Make login() a pure function with required parameters - Remove close_session() function - Provide migration guide for test scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.3 KiB
Auth Module Refactoring Design
Date: 2026-03-27 Status: Approved Author: Claude Code
Overview
Refactor the utils/auth.py module to follow decoupling principles by removing all environment variable dependencies and making it a pure, stateless authentication component.
Problem Statement
Current Issues
The utils/auth.py module has tight coupling to environment configuration:
- Module-level side effects: Loads environment variables on import (lines 10-22)
- Implicit defaults:
login()function reads from environment variables when parameters areNone - Hidden behavior: URL construction and browser path configuration embedded in the module
- Mixed responsibilities: Authentication logic mixed with configuration management
Design Principles Violated
- Separation of Concerns: Configuration and business logic are mixed
- Dependency Inversion: Module depends on concrete environment implementation
- Single Responsibility: Module handles both auth and configuration loading
Proposed Solution
Architecture
Transform utils/auth.py into a pure, stateless authentication module with:
- Zero environment variable dependencies
- All parameters required (no defaults from environment)
- No module-level initialization or side effects
- Explicit parameter passing only
Module Structure
# utils/auth.py - Pure Authentication Module
# Removed:
# - Environment variable loading (load_dotenv)
# - BASE_DIR and path calculation
# - os.getenv() calls
# - close_session() function
# Functions:
# login(playwright, username, password, url, headless, ignore_https_errors, verbose)
# logout(main_frame, verbose)
API Changes
login() Function
Before:
def login(
playwright: Playwright,
username: str = None, # Defaults to os.getenv("ERP_USERNAME")
password: str = None, # Defaults to os.getenv("ERP_PASSWORD")
url: str = None, # Defaults to os.getenv("ERP_URL") + path
headless: bool = None, # Defaults to os.getenv("ERP_HEADLESS")
ignore_https_errors: bool = None, # Defaults to os.getenv("ERP_IGNORE_HTTPS_ERRORS")
verbose: bool = True,
) -> tuple[Browser, BrowserContext, Page, Frame]:
After:
def login(
playwright: Playwright,
username: str,
password: str,
url: str,
headless: bool,
ignore_https_errors: bool,
verbose: bool = True,
) -> tuple[Browser, BrowserContext, Page, Frame]:
"""
Login to Yonyou BIP system
Args:
playwright: Playwright instance
username: Username (required)
password: Password (required)
url: Complete login page URL (required)
headless: Whether to use headless mode (required)
ignore_https_errors: Whether to ignore HTTPS errors (required)
verbose: Whether to print detailed logs (default: True)
Returns:
tuple: (browser, context, page, main_frame)
"""
Changes:
- All parameters become required (no defaults)
- URL parameter expects complete URL (no automatic path appending)
- Removed lines 59-76 (environment variable reading)
- Removed lines 62-63 (URL construction logic)
logout() Function
No changes - already a pure function.
close_session() Function
REMOVED ENTIRELY
Callers now manage browser lifecycle directly:
# Caller code
context.close()
browser.close()
Data Flow
Before:
Test Script → login() → [reads .env internally] → Browser
↑
(implicit config)
After:
Test Script → [loads .env] → [constructs URL] → login() → Browser
↓ ↓
(explicit config) (explicit params)
Implementation Details
Imports to Remove
# Remove these imports:
import os # Only used for os.getenv()
from dotenv import load_dotenv # No longer needed
from pathlib import Path # Only used for BASE_DIR calculation
Code to Remove
Lines 10-22: Environment setup
# DELETE THESE LINES:
# Load environment variables
from dotenv import load_dotenv
# Get project root directory
BASE_DIR = Path(__file__).resolve().parent.parent
# Load .env file
env_path = BASE_DIR / ".env"
load_dotenv(env_path)
# Configure Playwright browser path
browsers_path = os.getenv("PLAYWRIGHT_BROWSERS_PATH")
if browsers_path:
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = browsers_path
Lines 59-76: Environment variable defaults in login()
# DELETE THESE LINES:
# URL handling: read from environment variable or parameter
if not url:
url = os.getenv("ERP_URL")
if url and not url.endswith("login/main/index.html"):
url = url.rstrip("/") + "/yonbip/resources/uap/rbac/login/main/index.html"
if not url:
raise ValueError("URL must be provided either as parameter or through ERP_URL environment variable")
# headless parameter handling
if headless is None:
headless_str = os.getenv("ERP_HEADLESS", "false").lower()
headless = headless_str in ("true", "1", "yes")
# ignore_https_errors parameter handling
if ignore_https_errors is None:
ignore_https_errors_str = os.getenv("ERP_IGNORE_HTTPS_ERRORS", "true").lower()
ignore_https_errors = ignore_https_errors_str in ("true", "1", "yes")
Lines 161-177: close_session() function
# DELETE THIS ENTIRE FUNCTION
def close_session(browser: Browser, context: BrowserContext) -> None:
...
Code to Modify
login() signature - remove defaults:
# CHANGE FROM:
def login(playwright, username=None, password=None, url=None, headless=None, ignore_https_errors=None, verbose=True)
# CHANGE TO:
def login(playwright, username, password, url, headless, ignore_https_errors, verbose=True)
Add parameter validation:
if not username:
raise ValueError("username is required")
if not password:
raise ValueError("password is required")
if not url:
raise ValueError("url is required")
Testing Impact
Test Script Changes
All test scripts must be updated to:
- Load environment variables explicitly (already done)
- Construct complete URLs before calling
login() - Pass all parameters explicitly to
login() - Replace
close_session()calls with directcontext.close()andbrowser.close()
Example Migration
Before (test_login.py):
with sync_playwright() as p:
browser, context, page, main_frame = login(
playwright=p,
verbose=True
)
# ... use browser ...
close_session(browser, context)
After:
# Load and prepare config
url = f"{os.getenv('ERP_URL').rstrip('/')}/yonbip/resources/uap/rbac/login/main/index.html"
with sync_playwright() as p:
browser, context, page, main_frame = login(
playwright=p,
username=os.getenv('ERP_USERNAME'),
password=os.getenv('ERP_PASSWORD'),
url=url,
headless=os.getenv('ERP_HEADLESS', 'false').lower() in ('true', '1', 'yes'),
ignore_https_errors=os.getenv('ERP_IGNORE_HTTPS_ERRORS', 'true').lower() in ('true', '1', 'yes'),
verbose=True
)
# ... use browser ...
context.close()
browser.close()
Test Files to Update
tests/test_login.py- Update login() calls and close_session() usagetests/test_auth_config.py- No changes (doesn't call login())
Benefits
- Decoupling: Auth module independent of configuration source
- Testability: Easier to test with mock data
- Clarity: Explicit dependencies make data flow obvious
- Flexibility: Can be used with any configuration source (env, config file, CLI args, etc.)
- Purity: Functions have no hidden side effects
Migration Path
- Update
utils/auth.pywith all changes - Update
tests/test_login.pyto use new API - Run tests to verify functionality
- Update documentation (CLAUDE.md)
Risks and Mitigations
| Risk | Mitigation |
|---|---|
| Breaking existing scripts | Clear migration guide; tests updated first |
| Parameter verbosity | Tests already have env loading code |
| URL construction duplication | Document pattern in CLAUDE.md |
Acceptance Criteria
utils/auth.pyhas zeroos.getenv()callsutils/auth.pyhas noload_dotenv()callslogin()requires all parameters (no None defaults)close_session()function removed- All tests pass with new API
- CLAUDE.md documentation updated