Add CAPTCHA automation beta flow

This commit is contained in:
2026-10-08 17:36:30 +05:30
parent 631f64b84f
commit 24f2c1fe6a
7 changed files with 698 additions and 30 deletions
+3
View File
@@ -21,3 +21,6 @@ reports/logs/*
reports/html/* reports/html/*
!reports/html/.gitkeep !reports/html/.gitkeep
# CAPTCHA stress-test screenshots and CSV reports
reports/captcha_login_stress/
+6
View File
@@ -12,3 +12,9 @@ markers =
reports: report download coverage reports: report download coverage
order_book: order book coverage order_book: order book coverage
trade_book: trade book coverage trade_book: trade book coverage
captcha: CAPTCHA login stress test
filterwarnings =
ignore::DeprecationWarning:easyocr.*
ignore::DeprecationWarning:torch.*
ignore::UserWarning:torch.*
+10 -1
View File
@@ -1,4 +1,7 @@
import os
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.chrome.service import Service
def create_chrome_driver(): def create_chrome_driver():
@@ -10,5 +13,11 @@ def create_chrome_driver():
options.add_argument("--disable-features=CalculateNativeWinOcclusion") options.add_argument("--disable-features=CalculateNativeWinOcclusion")
options.add_argument("--disable-hang-monitor") options.add_argument("--disable-hang-monitor")
options.add_argument("--disable-popup-blocking") options.add_argument("--disable-popup-blocking")
options.add_argument("--disable-background-networking")
options.add_argument("--disable-component-update")
options.add_argument("--disable-sync")
options.add_argument("--disable-default-apps")
options.add_experimental_option("excludeSwitches", ["enable-logging"])
return webdriver.Chrome(options=options) service = Service(log_output=os.devnull)
return webdriver.Chrome(service=service, options=options)
+353
View File
@@ -0,0 +1,353 @@
"""
CAPTCHA Solver for PXIL Login using EasyOCR.
Extracts base64 from input's CSS background-image, runs EasyOCR OCR, fills the input.
"""
import re
import base64
import time
from typing import Optional
from PIL import Image
import cv2
import numpy as np
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
# Initialize EasyOCR once (module level)
# First run downloads models, then works offline
try:
import easyocr
_EASYOCR_READER = easyocr.Reader(['en'], gpu=False)
except ImportError:
_EASYOCR_READER = None
# Character confusion correction map for common OCR errors
CONFUSION_MAP = {
'0': 'O', 'O': '0', 'D': '0',
'1': 'I', 'I': '1', 'L': '1', 'l': '1',
'5': 'S', 'S': '5',
'8': 'B', 'B': '8',
'2': 'Z', 'Z': '2',
'6': 'G', 'G': '6',
'9': 'g', 'g': '9',
'Q': 'O',
'U': 'V', 'V': 'U',
'n': 'h', 'h': 'n',
'r': 'f', 'f': 'r',
'c': 'e', 'e': 'c',
'o': '0', '0': 'o',
's': '5', '5': 's',
'z': '2', '2': 'z',
'i': '1', '1': 'i',
'l': '1', '1': 'l',
}
def find_visible_captcha_input(driver: WebDriver) -> WebElement:
"""Find the visible captcha input element (name='captcha')."""
wait = WebDriverWait(driver, 15)
inputs = wait.until(EC.presence_of_all_elements_located((By.NAME, "captcha")))
for inp in inputs:
if inp.is_displayed():
return inp
raise RuntimeError("No visible captcha input found")
def extract_captcha_base64(captcha_input: WebElement) -> bytes:
"""Extract base64 image data from input's style attribute. Returns raw JPEG bytes."""
style = captcha_input.get_attribute("style") or ""
match = re.search(r'background:\s*url\(["\']?data:image/[^;]+;base64,([^"\')]+)["\']?\)', style)
if not match:
raise RuntimeError(f"No base64 background-image found in style: {style[:200]}")
b64_data = match.group(1)
if not b64_data:
raise RuntimeError("Base64 data is empty (captcha not loaded yet)")
b64_data += "=" * ((4 - len(b64_data) % 4) % 4) # Fix padding
return base64.b64decode(b64_data)
def wait_for_captcha_load(driver: WebDriver, captcha_input: WebElement, timeout: int = 15) -> bool:
"""Wait for captcha base64 to be populated with actual image data."""
end_time = time.time() + timeout
while time.time() < end_time:
try:
style = captcha_input.get_attribute("style") or ""
match = re.search(r'background:\s*url\(["\']?data:image/[^;]+;base64,([^"\')]+)["\']?\)', style)
if match and match.group(1):
return True
except StaleElementReferenceException:
captcha_input = find_visible_captcha_input(driver)
time.sleep(0.3)
return False
def preprocess_captcha_image(image_bytes: bytes) -> Image.Image:
"""Preprocess image for better OCR accuracy - gentle preprocessing."""
nparr = np.frombuffer(image_bytes, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) # Load as color (BGR)
if img is None:
raise RuntimeError("Failed to decode image bytes")
# Convert BGR to RGB
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# Simple threshold - keep it simple
_, gray = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY)
# Light denoise
gray = cv2.fastNlMeansDenoising(gray, h=3)
# Very light morphological close - only connect broken parts
kernel = np.ones((1, 1), np.uint8)
gray = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
# Resize 6x for better recognition (larger for 6-char captcha)
gray = cv2.resize(gray, None, fx=6, fy=6, interpolation=cv2.INTER_CUBIC)
# Convert back to RGB (3-channel) for EasyOCR
rgb = cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
return Image.fromarray(rgb)
def apply_confusion_corrections(text: str) -> str:
"""Apply character confusion corrections based on common OCR errors."""
corrected = []
for ch in text:
corrected.append(CONFUSION_MAP.get(ch, ch))
return ''.join(corrected)
def ocr_captcha(image: Image.Image) -> str:
"""Run OCR on preprocessed captcha image using EasyOCR."""
if _EASYOCR_READER is None:
raise RuntimeError("EasyOCR not installed. Run: pip install easyocr")
img_array = np.array(image)
# EasyOCR with alphanumeric allowlist for 6-char captcha
results = _EASYOCR_READER.readtext(
img_array,
detail=0,
allowlist='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
width_ths=0.5,
paragraph=False
)
# Combine all detected text
result = ''.join(results)
# Clean result - keep only alphanumeric, max 6 chars
cleaned = ''.join(filter(str.isalnum, result))[:6]
print(f" DEBUG OCR: result='{result}', cleaned='{cleaned}', len={len(cleaned)}")
return cleaned
def find_refresh_button(driver: WebDriver, captcha_input: WebElement) -> Optional[WebElement]:
"""Find captcha refresh button near the captcha input."""
parent = captcha_input.find_element(By.XPATH, "..")
for sibling in parent.find_elements(By.XPATH, "*"):
onclick = (sibling.get_attribute("onclick") or "").lower()
class_attr = (sibling.get_attribute("class") or "").lower()
id_attr = (sibling.get_attribute("id") or "").lower()
if any(kw in onclick for kw in ["refresh", "captcha", "reload", "newcaptcha"]):
return sibling
if any(kw in class_attr for kw in ["refresh", "captcha-refresh", "reload"]):
return sibling
if any(kw in id_attr for kw in ["refresh", "captcha", "reload"]):
return sibling
# Fallback: search globally for refresh-like elements
try:
candidates = driver.find_elements(By.XPATH, "//*[self::a or self::button or self::img or self::span][contains(translate(@onclick, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh') or contains(translate(@class, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh') or contains(translate(@title, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'refresh')]")
for cand in candidates:
if cand.is_displayed():
return cand
except:
pass
return None
def wait_for_new_captcha(driver: WebDriver, captcha_input: WebElement, timeout: int = 10) -> bool:
"""Wait for captcha background-image to change (new challenge loaded)."""
old_style = captcha_input.get_attribute("style") or ""
end_time = time.time() + timeout
while time.time() < end_time:
try:
new_style = captcha_input.get_attribute("style") or ""
if new_style != old_style and "data:image" in new_style:
return True
except StaleElementReferenceException:
captcha_input = find_visible_captcha_input(driver)
old_style = captcha_input.get_attribute("style") or ""
time.sleep(0.3)
return False
def solve_and_fill_captcha(driver: WebDriver, max_retries: int = 5, attempt_callback=None) -> bool:
"""
Main entry point: solve captcha and fill input.
Returns True if filled successfully, False if max retries exceeded.
Each retry gets a fresh captcha via refresh button.
"""
for attempt in range(1, max_retries + 1):
captcha_text = ""
try:
captcha_input = find_visible_captcha_input(driver)
# Wait for captcha base64 to be populated (async load)
if not wait_for_captcha_load(driver, captcha_input):
print(f" [Attempt {attempt}] Captcha failed to load in time, retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
if attempt_callback:
attempt_callback(attempt, "", "", "not_loaded")
continue
image_bytes = extract_captcha_base64(captcha_input)
pil_image = preprocess_captcha_image(image_bytes)
captcha_text = ocr_captcha(pil_image)
if not captcha_text or len(captcha_text) < 6:
print(f" [Attempt {attempt}] OCR weak result: '{captcha_text}' (len={len(captcha_text)}), retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
if attempt_callback:
attempt_callback(attempt, captcha_text, "", "ocr_failed")
continue
# Apply confusion corrections to get best variant
corrected = apply_confusion_corrections(captcha_text)
# Generate case variants for case-sensitive captcha (mixed case + numbers)
variants = [
captcha_text, # as-is (from OCR)
corrected, # with confusion corrections
captcha_text.upper(), # all uppercase
captcha_text.capitalize(), # first letter uppercase
captcha_text.title(), # each word capitalized
captcha_text.swapcase(), # swap case
]
# Add variations with first char variations
if len(captcha_text) >= 1:
variants.append(captcha_text[0].upper() + captcha_text[1:])
variants.append(captcha_text[0].lower() + captcha_text[1:])
if len(captcha_text) >= 2:
variants.append(captcha_text[:2].upper() + captcha_text[2:])
variants.append(captcha_text[:2].lower() + captcha_text[2:])
if len(captcha_text) >= 3:
variants.append(captcha_text[:3].upper() + captcha_text[3:])
variants.append(captcha_text[:3].lower() + captcha_text[3:])
# Remove duplicates while preserving order, keep only 6-char variants
seen = set()
unique_variants = []
for v in variants:
if v not in seen and len(v) == 6:
seen.add(v)
unique_variants.append(v)
variants = unique_variants
if attempt_callback:
screenshot_path = attempt_callback(attempt, captcha_text, "", "before_entry") or ""
success = False
# Try ALL variants before giving up on this attempt
for variant in variants:
if len(variant) == 6:
print(f" [Attempt {attempt}] Trying variant: '{variant}'")
captcha_input.clear()
captcha_input.send_keys(variant)
success = True
if attempt_callback:
attempt_callback(attempt, captcha_text, variant, "filled")
break
if not success:
print(f" [Attempt {attempt}] No valid 6-char variant found for '{captcha_text}', retrying...")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
# Click submit
submit_btn = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//button[@value='Login']"))
)
submit_btn.click()
# Wait a bit for response
time.sleep(2)
# Check for invalid captcha error using the exact xpath
try:
# First handle any alerts that might be open
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected after submit: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
invalid_captcha_elem = WebDriverWait(driver, 3).until(
EC.visibility_of_element_located((
By.XPATH,
"//div[@id='error']//strong[contains(normalize-space(), 'Invalid Captcha')]",
))
)
print(f" [Attempt {attempt}] Invalid captcha detected: {invalid_captcha_elem.text}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
except TimeoutException:
# No invalid captcha error - likely success
print(f" [Attempt {attempt}] No invalid captcha error detected, assuming success")
return True
except Exception as e:
# Handle other exceptions like NoSuchElementException
print(f" [Attempt {attempt}] Error checking invalid captcha: {type(e).__name__}: {e}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
continue
except Exception as e:
print(f" [Attempt {attempt}] Error: {e}")
if attempt < max_retries:
refresh_btn = find_refresh_button(driver, captcha_input)
if refresh_btn:
refresh_btn.click()
wait_for_new_captcha(driver, captcha_input)
return False
+190 -17
View File
@@ -57,43 +57,216 @@ def enter_pressed():
return pressed return pressed
def login(driver, username, password): def check_captcha_error(driver) -> str:
"""Check if there's a CAPTCHA error message on the page."""
try:
# First handle any alerts
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected in check_captcha_error: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
error_selectors = [
"//div[@id='error']//strong[normalize-space()='Invalid Captcha']",
"//div[@id='error']//span[contains(@class,'help-block')]//strong[contains(text(),'Invalid')]",
"//*[contains(@class,'error') and contains(text(),'Invalid Captcha')]",
]
for selector in error_selectors:
elements = driver.find_elements(By.XPATH, selector)
for el in elements:
if el.is_displayed() and el.text.strip():
return el.text.strip()
except Exception as e:
print(f" DEBUG: check_captcha_error exception: {e}")
return ""
def check_login_success(driver) -> bool:
"""Check if login was successful using multiple indicators."""
try:
# Handle any pending alerts first
try:
alert = driver.switch_to.alert
alert_text = alert.text
print(f" DEBUG: Alert detected: {alert_text}")
if "logout" in alert_text.lower() or "session" in alert_text.lower() or "previous" in alert_text.lower():
alert.accept()
print(" DEBUG: Accepted session/logout alert")
time.sleep(2)
except:
pass
# Wait a bit for page to load after submit
time.sleep(3)
# Print current URL and page title for debugging
print(f" DEBUG: Current URL: {driver.current_url}")
print(f" DEBUG: Page title: {driver.title}")
# Multiple dashboard indicators
dashboard_indicators = [
"//span[normalize-space()='TRADING MAINTENANCE']",
"//a[contains(@href, 'tradingMaintenance')]",
"//span[contains(text(), 'Dashboard')]",
"//h1[contains(text(), 'Dashboard')]",
"//div[contains(@class, 'dashboard')]",
"//a[contains(text(), 'Trading')]",
"//div[contains(@class, 'navbar')]//span[contains(text(), 'Trading')]",
]
wait = WebDriverWait(driver, 15)
for indicator in dashboard_indicators:
try:
wait.until(EC.visibility_of_element_located((By.XPATH, indicator)))
print(f" DEBUG: Found dashboard indicator: {indicator}")
return True
except:
continue
# URL-based check
if "home" in driver.current_url.lower() or "dashboard" in driver.current_url.lower():
print(" DEBUG: Login success detected via URL")
return True
return False
except Exception as e:
print(f" DEBUG: check_login_success failed: {e}")
return False
def login(
driver,
username,
password,
max_captcha_retries=5,
captcha_metrics=None,
max_login_duration_seconds=1200,
):
wait = WebDriverWait(driver, 20) wait = WebDriverWait(driver, 20)
# username # --- AUTOMATED CAPTCHA SOLVING WITH RETRY ON FAILURE ---
from tests.captcha_solver import solve_and_fill_captcha, find_refresh_button, wait_for_new_captcha
login_deadline = time.monotonic() + max_login_duration_seconds
captcha_attempt = 0
while True:
captcha_attempt += 1
# Refreshing after a failed CAPTCHA clears the login form, so refill
# both credentials before every automatic or manual retry.
userid_field = wait.until( userid_field = wait.until(
EC.visibility_of_element_located((By.XPATH, Locator.userid)) EC.visibility_of_element_located((By.XPATH, Locator.userid))
) )
userid_field.clear()
userid_field.send_keys(username) userid_field.send_keys(username)
# password password_field = wait.until(
password_field = driver.find_element(By.XPATH, Locator.password) EC.visibility_of_element_located((By.XPATH, Locator.password))
)
password_field.clear()
password_field.send_keys(password) password_field.send_keys(password)
print(f"Solve captcha within {LOGIN_CAPTCHA_WAIT_SECONDS} seconds...") if captcha_metrics is not None:
captcha_metrics["tries"] = captcha_attempt
captcha_metrics["max_retries"] = max_captcha_retries
print(f"Solving CAPTCHA (attempt {captcha_attempt})...")
# wait for manual captcha solve # Once automatic solving has run for the configured duration, ask for
time.sleep(LOGIN_CAPTCHA_WAIT_SECONDS) # the CAPTCHA value in a browser prompt and continue the same login flow.
if time.monotonic() >= login_deadline:
# click submit print("Automatic CAPTCHA retries timed out; waiting for manual CAPTCHA entry.")
submit_btn = wait.until( manual_value = driver.execute_script(
EC.element_to_be_clickable((By.XPATH, Locator.submitbtn)) "return window.prompt('Automatic CAPTCHA solving timed out. Enter the CAPTCHA shown on the page:');"
) )
submit_btn.click() if not manual_value:
raise RuntimeError("Manual CAPTCHA entry was cancelled")
captcha_input = wait.until(
EC.visibility_of_element_located((By.NAME, "captcha"))
)
captcha_input.clear()
captcha_input.send_keys(manual_value.strip())
wait.until(EC.element_to_be_clickable((By.XPATH, Locator.submitbtn))).click()
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
accept_browser_alert_if_present(driver)
handle_transient_popups(driver)
captcha_error = check_captcha_error(driver)
if captcha_error:
print(f" Manual CAPTCHA rejected: {captcha_error}")
continue
if check_login_success(driver):
print("Login successful after manual CAPTCHA entry")
if captcha_metrics is not None:
captcha_metrics["correct_attempt"] = captcha_attempt
captcha_metrics.setdefault("captcha_attempts", []).append({
"attempt": captcha_attempt,
"captcha_text": "manual",
"variant": manual_value.strip(),
"status": "manual",
"screenshot": "",
"error": "",
})
close_tac_expiry_modal_if_present(driver)
return True
continue
def attempt_callback(attempt_num, captcha_text, variant, status, screenshot_path="", error=""):
if captcha_metrics is not None:
captcha_metrics.setdefault("captcha_attempts", []).append({
"attempt": attempt_num,
"captcha_text": captcha_text,
"variant": variant,
"status": status,
"screenshot": screenshot_path,
"error": error
})
if not solve_and_fill_captcha(driver, max_retries=1, attempt_callback=attempt_callback):
print(" Automatic CAPTCHA solve failed; retrying until the login deadline.")
driver.refresh()
time.sleep(3)
continue
# solve_and_fill_captcha already clicks submit, so just wait and check result
time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS) time.sleep(LOGIN_POST_SUBMIT_WAIT_SECONDS)
accept_browser_alert_if_present(driver) accept_browser_alert_if_present(driver)
# wait for dashboard # Handle any popups that appear
wait.until( handle_transient_popups(driver)
EC.visibility_of_element_located((By.XPATH, Locator.dashboard_marker))
)
close_tac_expiry_modal_if_present(driver) # Check for CAPTCHA error (invalid captcha message)
captcha_error = check_captcha_error(driver)
if captcha_error:
print(f" CAPTCHA validation failed: {captcha_error}")
# Reload the page to get a fresh CAPTCHA and keep retrying until
# the overall login deadline is reached.
print(f" Reloading page to get fresh CAPTCHA (attempt {captcha_attempt + 1})...")
driver.refresh()
time.sleep(3)
continue
# Check for successful login - use improved detection
if check_login_success(driver):
print("Login successful") print("Login successful")
if captcha_metrics is not None:
captcha_metrics["correct_attempt"] = captcha_attempt
close_tac_expiry_modal_if_present(driver)
return True
# If we get here, something else is wrong
print(" Login not confirmed, retrying...")
driver.refresh()
time.sleep(3)
def accept_browser_alert_if_present(driver, timeout=5): def accept_browser_alert_if_present(driver, timeout=5):
+2 -2
View File
@@ -4,11 +4,11 @@ class Locator(object):
userid="//input[@placeholder='User ID']" userid="//input[@placeholder='User ID']"
password="//input[@placeholder='Password']" password="//input[@placeholder='Password']"
submitbtn="//button[@value='Login']" submitbtn="//button[@value='Login']"
dashboard_marker="//span[normalize-space()='TRADING MAINTENANCE']" dashboard_marker="//*[contains(normalize-space(), 'TRADING MAINTENANCE')]"
tacexpiryclosebtn="//div[contains(@class,'modal') and contains(@style,'display: block')]//button[contains(@class,'close')]" tacexpiryclosebtn="//div[contains(@class,'modal') and contains(@style,'display: block')]//button[contains(@class,'close')]"
usermenutoggle="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//a[contains(@class,'dropdown-toggle') and @data-toggle='dropdown']" usermenutoggle="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//a[contains(@class,'dropdown-toggle') and @data-toggle='dropdown']"
logoutlink="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//ul[contains(@class,'dropdown-menu')]//a[@ng-click='logout()' and normalize-space()='Log Out']" logoutlink="//li[contains(@class,'dropdown') and contains(@class,'navbar-user')]//ul[contains(@class,'dropdown-menu')]//a[@ng-click='logout()' and normalize-space()='Log Out']"
tradingmaintenance="//span[normalize-space()='TRADING MAINTENANCE']" tradingmaintenance="//*[contains(normalize-space(), 'TRADING MAINTENANCE')]"
tradingsession="//a[normalize-space()='Trading Session Master']" tradingsession="//a[normalize-space()='Trading Session Master']"
tokengeneration="//a[@href='#/tradingMaintenance/generateToken' or normalize-space()='Token Generation']" tokengeneration="//a[@href='#/tradingMaintenance/generateToken' or normalize-space()='Token Generation']"
tokengenerator = "//a[@href='#/tradingMaintenance/tokenGenerator']" tokengenerator = "//a[@href='#/tradingMaintenance/tokenGenerator']"
+124
View File
@@ -0,0 +1,124 @@
import csv
import os
import time
from datetime import datetime
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from tests.browser import create_chrome_driver
from tests.config_reader import load_config
from tests.helpers import login, logout
from tests.locators import Locator
LOGIN_ATTEMPTS = 50
MAX_CAPTCHA_RETRIES = 7
@pytest.mark.captcha
def test_captcha_login_stress():
"""Run bounded EasyOCR login attempts and retain auditable CAPTCHA evidence."""
config = load_config()
output_dir = os.path.abspath(os.path.join("reports", "captcha_login_stress"))
os.makedirs(output_dir, exist_ok=True)
started = datetime.now().strftime("%Y%m%d_%H%M%S")
csv_path = os.path.join(output_dir, f"captcha_login_{started}.csv")
rows = []
passed = 0
failed = 0
def record(row):
rows.append(row)
for login_number in range(1, LOGIN_ATTEMPTS + 1):
login_started = datetime.now().isoformat(timespec="seconds")
captcha_rows = []
driver = create_chrome_driver()
try:
driver.get(config["url"])
login_status = "FAILED"
logout_status = "NOT_ATTEMPTED"
error_text = ""
captcha_metrics = {"captcha_attempts": []}
try:
login(
driver,
config["login"]["username"],
config["login"]["password"],
max_captcha_retries=MAX_CAPTCHA_RETRIES,
captcha_metrics=captcha_metrics,
max_login_duration_seconds=1200,
)
login_status = "PASSED"
passed += 1
except Exception as exc:
failed += 1
error_text = f"{type(exc).__name__}: {exc}"
try:
driver.save_screenshot(os.path.join(output_dir, f"login_{login_number:03d}_failure.png"))
except Exception:
pass
finally:
# Explicitly log out after a successful authenticated flow,
# then close the browser session.
if login_status == "PASSED":
try:
logout_status = "PASSED" if logout(driver) else "FAILED"
except Exception as exc:
logout_status = "FAILED"
print(f"Logout error: {type(exc).__name__}: {exc}")
try:
driver.quit()
except Exception:
pass
captcha_rows = captcha_metrics.get("captcha_attempts", []) or [{
"attempt": 0, "captcha_text": "", "variant": "",
"status": "no_captcha_record", "screenshot": "", "error": error_text,
}]
for item in captcha_rows:
record({
"login_attempt": login_number,
"started_at": login_started,
"login_status": login_status,
"logout_status": logout_status,
"captcha_attempt": item["attempt"],
"attempted_captcha": item["variant"],
"ocr_captcha": item["captcha_text"],
"screenshot_path": item["screenshot"],
"captcha_status": item["status"],
"captcha_entered_count": sum(
1 for captcha_row in captcha_rows if captcha_row["variant"]
),
"error": item["error"] or error_text,
"attempts_to_result": len(captcha_rows),
"passed_count": passed,
"failed_count": failed,
})
finally:
# Clean up the driver after all attempts
try:
driver.quit()
except Exception:
pass
fieldnames = [
"login_attempt", "started_at", "login_status", "captcha_attempt",
"logout_status",
"attempted_captcha", "ocr_captcha", "screenshot_path", "captcha_status",
"captcha_entered_count", "error", "attempts_to_result", "passed_count",
"failed_count",
]
with open(csv_path, "w", newline="", encoding="utf-8") as report:
writer = csv.DictWriter(report, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
print(f"CAPTCHA stress report: {csv_path}")
print(f"Passed: {passed}; Failed: {failed}; Total: {LOGIN_ATTEMPTS}")