- Add SPA support for Playwright with wait_for_network_idle and extra_wait_ms - Add BaseStore.get_spa_config() and requires_playwright() methods - Implement AliExpress SPA config with JSON price extraction patterns - Fix Amazon price parsing to prioritize whole+fraction combination - Fix AliExpress regex patterns (remove double backslashes) - Add CLI tests: detect, doctor, fetch, parse, run commands - Add API tests: auth, logs, products, scraping_logs, webhooks Tests: 417 passed, 85% coverage Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
259 lines
8.0 KiB
Python
259 lines
8.0 KiB
Python
"""Tests pour la commande CLI run."""
|
|
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
import pytest
|
|
from typer.testing import CliRunner
|
|
|
|
from pricewatch.app.cli.main import app
|
|
from pricewatch.app.core.schema import ProductSnapshot, DebugInfo, DebugStatus, FetchMethod
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
@pytest.fixture
|
|
def yaml_config(tmp_path: Path) -> Path:
|
|
"""Cree un fichier YAML de config temporaire."""
|
|
yaml_content = """
|
|
urls:
|
|
- "https://www.amazon.fr/dp/B08N5WRWNW"
|
|
options:
|
|
use_playwright: false
|
|
force_playwright: false
|
|
headful: false
|
|
save_html: false
|
|
save_screenshot: false
|
|
timeout_ms: 30000
|
|
"""
|
|
file_path = tmp_path / "test_config.yaml"
|
|
file_path.write_text(yaml_content, encoding="utf-8")
|
|
return file_path
|
|
|
|
|
|
@pytest.fixture
|
|
def output_json(tmp_path: Path) -> Path:
|
|
"""Chemin pour le fichier JSON de sortie."""
|
|
return tmp_path / "output.json"
|
|
|
|
|
|
class TestRunCommand:
|
|
"""Tests pour la commande run."""
|
|
|
|
@patch("pricewatch.app.cli.main.fetch_http")
|
|
def test_run_http_success(self, mock_fetch, yaml_config, output_json):
|
|
"""Run avec HTTP reussi."""
|
|
# Mock HTTP fetch
|
|
mock_result = MagicMock()
|
|
mock_result.success = True
|
|
mock_result.html = """
|
|
<html><body>
|
|
<span id="productTitle">Test Product</span>
|
|
<span class="a-price-whole">299,99 €</span>
|
|
</body></html>
|
|
"""
|
|
mock_result.error = None
|
|
mock_fetch.return_value = mock_result
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["run", "--yaml", str(yaml_config), "--out", str(output_json), "--no-db"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
assert output_json.exists()
|
|
|
|
@patch("pricewatch.app.cli.main.fetch_http")
|
|
@patch("pricewatch.app.cli.main.fetch_playwright")
|
|
def test_run_http_fail_playwright_fallback(
|
|
self, mock_pw, mock_http, yaml_config, output_json
|
|
):
|
|
"""Run avec fallback Playwright quand HTTP echoue."""
|
|
# Mock HTTP fail
|
|
mock_http_result = MagicMock()
|
|
mock_http_result.success = False
|
|
mock_http_result.error = "403 Forbidden"
|
|
mock_http.return_value = mock_http_result
|
|
|
|
# Mock Playwright success
|
|
mock_pw_result = MagicMock()
|
|
mock_pw_result.success = True
|
|
mock_pw_result.html = """
|
|
<html><body>
|
|
<span id="productTitle">Playwright Product</span>
|
|
<span class="a-price-whole">199,99 €</span>
|
|
</body></html>
|
|
"""
|
|
mock_pw_result.screenshot = None
|
|
mock_pw.return_value = mock_pw_result
|
|
|
|
# Modifier config pour activer playwright
|
|
yaml_content = """
|
|
urls:
|
|
- "https://www.amazon.fr/dp/B08N5WRWNW"
|
|
options:
|
|
use_playwright: true
|
|
force_playwright: false
|
|
headful: false
|
|
save_html: false
|
|
save_screenshot: false
|
|
timeout_ms: 30000
|
|
"""
|
|
yaml_config.write_text(yaml_content, encoding="utf-8")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["run", "--yaml", str(yaml_config), "--out", str(output_json), "--no-db"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
mock_pw.assert_called()
|
|
|
|
@patch("pricewatch.app.cli.main.fetch_http")
|
|
def test_run_http_fail_no_playwright(self, mock_http, yaml_config, output_json):
|
|
"""Run avec HTTP echoue sans Playwright."""
|
|
mock_result = MagicMock()
|
|
mock_result.success = False
|
|
mock_result.error = "Connection refused"
|
|
mock_http.return_value = mock_result
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["run", "--yaml", str(yaml_config), "--out", str(output_json), "--no-db"],
|
|
)
|
|
|
|
# Doit quand meme creer le fichier JSON (avec snapshot failed)
|
|
assert result.exit_code == 0
|
|
assert output_json.exists()
|
|
|
|
def test_run_invalid_yaml(self, tmp_path, output_json):
|
|
"""Run avec YAML invalide echoue."""
|
|
yaml_file = tmp_path / "invalid.yaml"
|
|
yaml_file.write_text("invalid: [yaml: content", encoding="utf-8")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["run", "--yaml", str(yaml_file), "--out", str(output_json)],
|
|
)
|
|
|
|
assert result.exit_code == 1
|
|
|
|
def test_run_with_debug(self, yaml_config, output_json):
|
|
"""Run avec --debug active les logs."""
|
|
with patch("pricewatch.app.cli.main.fetch_http") as mock_fetch:
|
|
mock_result = MagicMock()
|
|
mock_result.success = True
|
|
mock_result.html = "<html><body>Test</body></html>"
|
|
mock_fetch.return_value = mock_result
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
[
|
|
"run",
|
|
"--yaml",
|
|
str(yaml_config),
|
|
"--out",
|
|
str(output_json),
|
|
"--debug",
|
|
"--no-db",
|
|
],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
|
|
@patch("pricewatch.app.cli.main.fetch_playwright")
|
|
def test_run_force_playwright(self, mock_pw, tmp_path, output_json):
|
|
"""Run avec force_playwright skip HTTP."""
|
|
yaml_content = """
|
|
urls:
|
|
- "https://www.amazon.fr/dp/B08N5WRWNW"
|
|
options:
|
|
use_playwright: true
|
|
force_playwright: true
|
|
headful: false
|
|
save_html: false
|
|
save_screenshot: false
|
|
timeout_ms: 30000
|
|
"""
|
|
yaml_file = tmp_path / "force_pw.yaml"
|
|
yaml_file.write_text(yaml_content, encoding="utf-8")
|
|
|
|
mock_result = MagicMock()
|
|
mock_result.success = True
|
|
mock_result.html = "<html><body>PW content</body></html>"
|
|
mock_result.screenshot = None
|
|
mock_pw.return_value = mock_result
|
|
|
|
with patch("pricewatch.app.cli.main.fetch_http") as mock_http:
|
|
result = runner.invoke(
|
|
app,
|
|
["run", "--yaml", str(yaml_file), "--out", str(output_json), "--no-db"],
|
|
)
|
|
|
|
# HTTP ne doit pas etre appele
|
|
mock_http.assert_not_called()
|
|
mock_pw.assert_called()
|
|
assert result.exit_code == 0
|
|
|
|
@patch("pricewatch.app.cli.main.fetch_http")
|
|
def test_run_unknown_store(self, mock_fetch, tmp_path, output_json):
|
|
"""Run avec URL de store inconnu."""
|
|
yaml_content = """
|
|
urls:
|
|
- "https://www.unknown-store.com/product/123"
|
|
options:
|
|
use_playwright: false
|
|
"""
|
|
yaml_file = tmp_path / "unknown.yaml"
|
|
yaml_file.write_text(yaml_content, encoding="utf-8")
|
|
|
|
result = runner.invoke(
|
|
app,
|
|
["run", "--yaml", str(yaml_file), "--out", str(output_json), "--no-db"],
|
|
)
|
|
|
|
# Doit continuer sans crash
|
|
assert result.exit_code == 0
|
|
# HTTP ne doit pas etre appele (store non trouve)
|
|
mock_fetch.assert_not_called()
|
|
|
|
@patch("pricewatch.app.cli.main.fetch_http")
|
|
@patch("pricewatch.app.cli.main.fetch_playwright")
|
|
def test_run_with_save_screenshot(self, mock_pw, mock_http, tmp_path, output_json):
|
|
"""Run avec save_screenshot."""
|
|
yaml_content = """
|
|
urls:
|
|
- "https://www.amazon.fr/dp/B08N5WRWNW"
|
|
options:
|
|
use_playwright: true
|
|
force_playwright: false
|
|
save_screenshot: true
|
|
timeout_ms: 30000
|
|
"""
|
|
yaml_file = tmp_path / "screenshot.yaml"
|
|
yaml_file.write_text(yaml_content, encoding="utf-8")
|
|
|
|
# HTTP fail
|
|
mock_http_result = MagicMock()
|
|
mock_http_result.success = False
|
|
mock_http_result.error = "blocked"
|
|
mock_http.return_value = mock_http_result
|
|
|
|
# PW success avec screenshot
|
|
mock_pw_result = MagicMock()
|
|
mock_pw_result.success = True
|
|
mock_pw_result.html = "<html><body>content</body></html>"
|
|
mock_pw_result.screenshot = b"fake_png_data"
|
|
mock_pw.return_value = mock_pw_result
|
|
|
|
with patch("pricewatch.app.core.io.save_debug_screenshot") as mock_save:
|
|
result = runner.invoke(
|
|
app,
|
|
["run", "--yaml", str(yaml_file), "--out", str(output_json), "--no-db"],
|
|
)
|
|
|
|
assert result.exit_code == 0
|
|
# Le screenshot doit etre sauvegarde si present
|
|
mock_save.assert_called()
|