feat(pchome): automate authorized sales acquisition
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
This commit is contained in:
@@ -94,12 +94,14 @@ def test_daily_sales_import_fails_when_monthly_sync_fails(monkeypatch, tmp_path)
|
||||
job = session.query(import_service.ImportJob).filter_by(id=job_id).one()
|
||||
assert job.status == "failed"
|
||||
assert job.progress_percent == 95
|
||||
assert job.current_step == "業績分析儀表板同步失敗"
|
||||
assert job.current_step == "交易驗證失敗,已回滾"
|
||||
assert "monthly sync boom" in job.error_message
|
||||
summary = json.loads(job.import_summary)
|
||||
assert summary["sync_success"] is False
|
||||
assert summary["synced_to"] is None
|
||||
assert "monthly sync boom" in summary["sync_error"]
|
||||
assert summary["atomic"] is True
|
||||
assert summary["rolled_back"] is True
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -107,11 +109,45 @@ def test_daily_sales_import_fails_when_monthly_sync_fails(monkeypatch, tmp_path)
|
||||
snapshot_rows = conn.execute(text("SELECT COUNT(*) FROM daily_sales_snapshot")).scalar()
|
||||
monthly_rows = conn.execute(text("SELECT COUNT(*) FROM realtime_sales_monthly")).scalar()
|
||||
|
||||
assert snapshot_rows == 1
|
||||
assert snapshot_rows == 0
|
||||
assert monthly_rows == 0
|
||||
assert FakeNotificationManager.sent_messages
|
||||
|
||||
|
||||
def test_daily_sales_import_preserves_today_fallback_when_date_column_is_missing(monkeypatch, tmp_path):
|
||||
import_service = _load_import_service(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
_prepare_daily_sales_tables(import_service)
|
||||
source_df = pd.DataFrame([{
|
||||
"商品ID": "A001",
|
||||
"商品名稱": "測試商品",
|
||||
"銷售金額": 1200,
|
||||
}])
|
||||
monkeypatch.setattr(
|
||||
import_service,
|
||||
"_read_daily_sales_excel",
|
||||
lambda _path: (
|
||||
source_df.copy(),
|
||||
{"date_col": None, "sheet_name": "即時業績明細", "header_row": 1},
|
||||
),
|
||||
)
|
||||
service = import_service.ImportService()
|
||||
job_id = service.create_import_job("daily_sales", "drive-file-1", "daily.xlsx", 1024)
|
||||
|
||||
assert service.process_daily_sales_import(job_id, str(tmp_path / "daily.xlsx")) is True
|
||||
|
||||
expected_date = import_service.datetime.now(import_service.TAIPEI_TZ).date().isoformat()
|
||||
with import_service.engine.connect() as conn:
|
||||
snapshot = conn.execute(text(
|
||||
'SELECT "日期", snapshot_date FROM daily_sales_snapshot'
|
||||
)).one()
|
||||
monthly = conn.execute(text(
|
||||
'SELECT "日期" FROM realtime_sales_monthly'
|
||||
)).one()
|
||||
assert snapshot[0] == expected_date
|
||||
assert str(snapshot[1])[:10] == expected_date
|
||||
assert monthly[0] == expected_date
|
||||
|
||||
|
||||
def test_auto_import_does_not_move_drive_file_when_import_fails(monkeypatch, tmp_path):
|
||||
import_service = _load_import_service(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
import_service.Base.metadata.create_all(import_service.engine)
|
||||
@@ -145,6 +181,47 @@ def test_auto_import_does_not_move_drive_file_when_import_fails(monkeypatch, tmp
|
||||
assert fake_drive.moved_files == []
|
||||
|
||||
|
||||
def test_auto_import_reports_partial_when_drive_archive_fails(monkeypatch, tmp_path):
|
||||
import_service = _load_import_service(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
import_service.Base.metadata.create_all(import_service.engine)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
class FakeDriveService:
|
||||
last_error_kind = None
|
||||
last_error = None
|
||||
|
||||
def list_files_in_folder(self, folder_path, file_pattern):
|
||||
return [{"id": "drive-file-1", "name": "daily.xlsx", "size": 1024}]
|
||||
|
||||
def download_file(self, file_id, local_path):
|
||||
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
||||
with open(local_path, "wb") as handle:
|
||||
handle.write(b"test")
|
||||
return True
|
||||
|
||||
def move_file(self, file_id, folder, create_missing=False):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(import_service, "drive_service", FakeDriveService())
|
||||
service = import_service.ImportService()
|
||||
monkeypatch.setattr(service, "process_daily_sales_import", lambda job_id, path: True)
|
||||
|
||||
result = service.auto_import_from_drive()
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["status"] == "partial_source_finalize_failed"
|
||||
assert result["imported_count"] == 1
|
||||
assert result["source_finalize_ok"] is False
|
||||
assert result["archive_failed_count"] == 1
|
||||
session = import_service.Session()
|
||||
try:
|
||||
job = session.query(import_service.ImportJob).one()
|
||||
assert job.status == "completed"
|
||||
assert job.current_step == "匯入完成,來源封存待自動重試"
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def test_auto_import_fails_closed_when_drive_auth_fails(monkeypatch, tmp_path):
|
||||
import_service = _load_import_service(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
import_service.Base.metadata.create_all(import_service.engine)
|
||||
|
||||
@@ -793,8 +793,15 @@ def test_utility_pages_keep_operator_copy_professional():
|
||||
|
||||
assert "簡報線上預覽" in ppt_preview
|
||||
assert "下載簡報檔" in ppt_history
|
||||
assert "送出後更新日報、成長分析與今日作戰清單" in auto_import
|
||||
assert "等待系統更新任務狀態;若重複停在異常" in auto_import
|
||||
assert "業績資料自動化" in auto_import
|
||||
assert "正式業績可供分析與決策" in auto_import
|
||||
assert "雙表已驗證" in auto_import
|
||||
assert "自動安全執行" in auto_import
|
||||
assert "Controlled apply" not in auto_import
|
||||
assert "AUTHORIZED SOURCES" not in auto_import
|
||||
assert "VERIFIED RUNS" not in auto_import
|
||||
assert "手動上傳匯入" not in auto_import
|
||||
assert "uploadManualFile" not in auto_import
|
||||
assert "return raw ||" not in auto_import
|
||||
assert "供貨風險匯入" in stockout_import
|
||||
assert "缺少必要內容時,會先停止匯入" in stockout_import
|
||||
|
||||
@@ -29,11 +29,23 @@ def test_daily_snapshot_delete_casts_text_date_column_on_postgres(monkeypatch):
|
||||
def test_daily_snapshot_delete_query_uses_snapshot_date_cast_helper():
|
||||
source = Path("services/import_service.py").read_text(encoding="utf-8")
|
||||
|
||||
assert 'snapshot_date_expr = _date_filter_expr("snapshot_date")' in source
|
||||
assert "DELETE FROM {table_name} WHERE {snapshot_date_expr} IN" in source
|
||||
assert 'snapshot_expr = _date_filter_expr("snapshot_date")' in source
|
||||
assert "f\"WHERE {snapshot_expr} IN ({snapshot_placeholders})\"" in source
|
||||
assert "DELETE FROM daily_sales_snapshot WHERE snapshot_date IN" not in source
|
||||
|
||||
|
||||
def test_sales_table_replacement_uses_one_transaction_connection():
|
||||
source = Path("services/import_service.py").read_text(encoding="utf-8")
|
||||
start = source.index("def _replace_sales_tables_atomic")
|
||||
end = source.index("# 資料庫設定", start)
|
||||
block = source[start:end]
|
||||
|
||||
assert "with engine.begin() as conn:" in block
|
||||
assert 'df.to_sql(\n "daily_sales_snapshot",\n conn,' in block
|
||||
assert 'df_monthly.to_sql(\n "realtime_sales_monthly",\n conn,' in block
|
||||
assert "conn.commit()" not in block
|
||||
|
||||
|
||||
def test_daily_snapshot_delete_uses_iso_dates_on_sqlite(monkeypatch):
|
||||
monkeypatch.setattr(import_service, "_db_dialect_name", lambda: "sqlite")
|
||||
|
||||
|
||||
@@ -728,7 +728,7 @@ def test_primary_pages_use_growth_outcome_copy_instead_of_feature_explaining():
|
||||
expected = {
|
||||
"templates/daily_sales.html": "找出下滑與價差壓力",
|
||||
"templates/ai_recommend.html": "把價差、商品證據與趨勢轉成主推、調價、補比價動作",
|
||||
"templates/auto_import_index.html": "保持 PChome 業績新鮮",
|
||||
"templates/auto_import_index.html": "正式業績可供分析與決策",
|
||||
"templates/price_comparison.html": "確認同款、判斷價差、決定下一步",
|
||||
"templates/vendor_stockout_index_v2.html": "避免主推商品斷貨",
|
||||
"templates/monthly_summary_analysis.html": "判斷成長、毛利與品類結構",
|
||||
@@ -889,14 +889,14 @@ def test_governance_and_low_frequency_pages_avoid_engineering_status_copy():
|
||||
"templates/403.html": ["權限守門", "未授權操作影響營運資料", "權限控管"],
|
||||
"templates/maintenance.html": ["服務維護", "確認業績、比價與匯入狀態", "台北時間"],
|
||||
"templates/auto_import_index.html": [
|
||||
"更新日報、成長分析與今日作戰清單",
|
||||
"送出後更新日報、成長分析與今日作戰清單",
|
||||
"作戰清單保持新鮮",
|
||||
"共更新",
|
||||
"buildImportActionHint",
|
||||
"重新確認 Google Drive 授權",
|
||||
"改用當日業績明細檔",
|
||||
"重新匯入最新檔案",
|
||||
"業績資料自動化",
|
||||
"來源覆蓋",
|
||||
"自動安全執行",
|
||||
"執行自動取得",
|
||||
"雙表已驗證",
|
||||
"等待最新業績",
|
||||
"loadReadiness",
|
||||
"/api/auto_import/run",
|
||||
],
|
||||
"templates/settings.html": ["比價來源同步", "補齊 MOMO 參考來源"],
|
||||
"templates/system_settings.html": [
|
||||
|
||||
334
tests/test_pchome_sales_acquisition_service.py
Normal file
334
tests/test_pchome_sales_acquisition_service.py
Normal file
@@ -0,0 +1,334 @@
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import pandas as pd
|
||||
import pytz
|
||||
from flask import Flask
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
TAIPEI_TZ = pytz.timezone("Asia/Taipei")
|
||||
|
||||
|
||||
def _load_services(monkeypatch, database_url):
|
||||
os.environ.setdefault("MOMO_ALLOW_INSECURE_CONFIG_FOR_TESTS", "true")
|
||||
import config
|
||||
|
||||
monkeypatch.setattr(config, "DATABASE_PATH", database_url)
|
||||
import services.import_service as import_service
|
||||
|
||||
import_service = importlib.reload(import_service)
|
||||
import services.pchome_sales_acquisition_service as acquisition
|
||||
|
||||
acquisition = importlib.reload(acquisition)
|
||||
return import_service, acquisition
|
||||
|
||||
|
||||
def _prepare_tables(import_service):
|
||||
import_service.Base.metadata.create_all(import_service.engine)
|
||||
with import_service.engine.begin() as conn:
|
||||
conn.execute(text("DROP TABLE IF EXISTS daily_sales_snapshot"))
|
||||
conn.execute(text("DROP TABLE IF EXISTS realtime_sales_monthly"))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE daily_sales_snapshot (
|
||||
"日期" TEXT,
|
||||
"商品ID" TEXT,
|
||||
"商品名稱" TEXT,
|
||||
"銷售金額" INTEGER,
|
||||
snapshot_date TEXT
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE realtime_sales_monthly (
|
||||
"日期" TEXT,
|
||||
"商品ID" TEXT,
|
||||
"商品名稱" TEXT,
|
||||
"銷售金額" INTEGER
|
||||
)
|
||||
"""))
|
||||
|
||||
|
||||
class EmptyDrive:
|
||||
last_error_kind = None
|
||||
last_error = None
|
||||
|
||||
def list_files_in_folder(self, _folder_path, _file_pattern):
|
||||
return []
|
||||
|
||||
|
||||
class ReadyDrive(EmptyDrive):
|
||||
def check_auth_readiness(self, refresh_expired=False):
|
||||
return {"ready": True, "kind": "ready"}
|
||||
|
||||
|
||||
class FailedDrive(EmptyDrive):
|
||||
last_error_kind = "authentication_failed"
|
||||
last_error = "credentials missing"
|
||||
|
||||
|
||||
def _write_sales_file(path):
|
||||
today = datetime.now(TAIPEI_TZ).strftime("%Y-%m-%d")
|
||||
pd.DataFrame([{
|
||||
"日期": today,
|
||||
"商品ID": "P001",
|
||||
"商品名稱": "測試商品",
|
||||
"銷售金額": 1680,
|
||||
}]).to_excel(path, index=False, sheet_name="即時業績明細")
|
||||
|
||||
|
||||
def _disable_remote_sources(monkeypatch):
|
||||
monkeypatch.setenv("PCHOME_SALES_HTTP_ENABLED", "false")
|
||||
monkeypatch.setenv("PCHOME_SALES_IMAP_ENABLED", "false")
|
||||
|
||||
|
||||
def test_local_drop_closes_import_and_archives_only_after_verification(monkeypatch, tmp_path):
|
||||
import_service, acquisition = _load_services(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
_prepare_tables(import_service)
|
||||
_disable_remote_sources(monkeypatch)
|
||||
drop_dir = tmp_path / "drop"
|
||||
drop_dir.mkdir()
|
||||
source = drop_dir / "即時業績_當日.xlsx"
|
||||
_write_sales_file(source)
|
||||
monkeypatch.setenv("PCHOME_SALES_LOCAL_DROP_DIR", str(drop_dir))
|
||||
monkeypatch.setattr(import_service, "drive_service", EmptyDrive())
|
||||
|
||||
result = acquisition.PChomeSalesAcquisitionService().run(trigger="test")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["status"] == "completed"
|
||||
assert result["source_type"] == "controlled_local_drop"
|
||||
assert result["total_rows"] == 1
|
||||
assert result["decision_ready"] is True
|
||||
assert result["receipt_persisted"] is True
|
||||
assert len(result["trace_id"]) == 32
|
||||
assert len(result["span_id"]) == 16
|
||||
assert result["trace_id"] == result["trace_id"].lower()
|
||||
assert result["span_id"] == result["span_id"].lower()
|
||||
assert not source.exists()
|
||||
assert (drop_dir / "archive" / source.name).exists()
|
||||
|
||||
with import_service.engine.connect() as conn:
|
||||
assert conn.execute(text("SELECT COUNT(*) FROM daily_sales_snapshot")).scalar() == 1
|
||||
assert conn.execute(text("SELECT COUNT(*) FROM realtime_sales_monthly")).scalar() == 1
|
||||
assert conn.execute(text("SELECT COUNT(*) FROM pchome_sales_acquisition_receipts")).scalar() == 1
|
||||
|
||||
|
||||
def test_same_content_is_idempotent_and_does_not_duplicate_rows(monkeypatch, tmp_path):
|
||||
import_service, acquisition = _load_services(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
_prepare_tables(import_service)
|
||||
_disable_remote_sources(monkeypatch)
|
||||
drop_dir = tmp_path / "drop"
|
||||
drop_dir.mkdir()
|
||||
source = drop_dir / "即時業績_當日.xlsx"
|
||||
_write_sales_file(source)
|
||||
original_bytes = source.read_bytes()
|
||||
monkeypatch.setenv("PCHOME_SALES_LOCAL_DROP_DIR", str(drop_dir))
|
||||
monkeypatch.setattr(import_service, "drive_service", EmptyDrive())
|
||||
service = acquisition.PChomeSalesAcquisitionService()
|
||||
|
||||
first = service.run(trigger="test")
|
||||
source.write_bytes(original_bytes)
|
||||
second = service.run(trigger="test")
|
||||
|
||||
assert first["status"] == "completed"
|
||||
assert second["status"] == "completed_no_write"
|
||||
assert second["duplicate_count"] == 1
|
||||
assert second["imported_count"] == 0
|
||||
with import_service.engine.connect() as conn:
|
||||
assert conn.execute(text("SELECT COUNT(*) FROM daily_sales_snapshot")).scalar() == 1
|
||||
assert conn.execute(text("SELECT COUNT(*) FROM realtime_sales_monthly")).scalar() == 1
|
||||
|
||||
|
||||
def test_verified_import_is_partial_when_source_archive_ack_fails(monkeypatch, tmp_path):
|
||||
import_service, acquisition = _load_services(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
import services.pchome_sales_acquisition_providers as providers
|
||||
|
||||
_prepare_tables(import_service)
|
||||
_disable_remote_sources(monkeypatch)
|
||||
drop_dir = tmp_path / "drop"
|
||||
drop_dir.mkdir()
|
||||
source = drop_dir / "即時業績_當日.xlsx"
|
||||
_write_sales_file(source)
|
||||
monkeypatch.setenv("PCHOME_SALES_LOCAL_DROP_DIR", str(drop_dir))
|
||||
monkeypatch.setattr(import_service, "drive_service", EmptyDrive())
|
||||
monkeypatch.setattr(
|
||||
providers,
|
||||
"_archive_file",
|
||||
lambda *_args, **_kwargs: (_ for _ in ()).throw(PermissionError("archive denied")),
|
||||
)
|
||||
|
||||
result = acquisition.PChomeSalesAcquisitionService().run(trigger="test")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["status"] == "partial"
|
||||
assert result["source_finalize_failed_count"] == 1
|
||||
assert result["safe_next_action"] == "retry_source_finalization"
|
||||
assert source.exists()
|
||||
with import_service.engine.connect() as conn:
|
||||
assert conn.execute(text("SELECT COUNT(*) FROM daily_sales_snapshot")).scalar() == 1
|
||||
assert conn.execute(text("SELECT COUNT(*) FROM realtime_sales_monthly")).scalar() == 1
|
||||
|
||||
|
||||
def test_no_candidate_with_missing_freshness_is_blocked_and_receipted(monkeypatch, tmp_path):
|
||||
import_service, acquisition = _load_services(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
_prepare_tables(import_service)
|
||||
_disable_remote_sources(monkeypatch)
|
||||
monkeypatch.delenv("PCHOME_SALES_LOCAL_DROP_DIR", raising=False)
|
||||
monkeypatch.setattr(import_service, "drive_service", EmptyDrive())
|
||||
|
||||
result = acquisition.PChomeSalesAcquisitionService().run(trigger="test")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["status"] == "blocked_with_safe_next_action"
|
||||
assert result["safe_next_action"] == "scheduler_retry_authorized_sources"
|
||||
assert result["receipt_persisted"] is True
|
||||
with import_service.engine.connect() as conn:
|
||||
receipt = conn.execute(text(
|
||||
"SELECT status, decision FROM pchome_sales_acquisition_receipts"
|
||||
)).one()
|
||||
assert receipt.status == "blocked_with_safe_next_action"
|
||||
assert receipt.decision == "no_authorized_candidate"
|
||||
|
||||
|
||||
def test_provider_failure_keeps_product_message_compact(monkeypatch, tmp_path):
|
||||
import_service, acquisition = _load_services(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
_prepare_tables(import_service)
|
||||
_disable_remote_sources(monkeypatch)
|
||||
monkeypatch.delenv("PCHOME_SALES_LOCAL_DROP_DIR", raising=False)
|
||||
monkeypatch.setattr(import_service, "drive_service", FailedDrive())
|
||||
|
||||
result = acquisition.PChomeSalesAcquisitionService().run(trigger="test")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["status"] == "degraded_no_write"
|
||||
assert result["message"] == "授權來源目前無法取得報表,系統將自動重試。"
|
||||
assert result["providers"][0]["error_kind"] == "authentication_failed"
|
||||
|
||||
|
||||
def test_readiness_never_exposes_provider_credentials_or_urls(monkeypatch, tmp_path):
|
||||
import_service, acquisition = _load_services(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
|
||||
_prepare_tables(import_service)
|
||||
monkeypatch.setattr(acquisition, "drive_service", ReadyDrive())
|
||||
monkeypatch.setenv("PCHOME_SALES_HTTP_ENABLED", "true")
|
||||
monkeypatch.setenv("PCHOME_SALES_HTTP_URL", "https://reports.example.test/private/report.xlsx")
|
||||
monkeypatch.setenv("PCHOME_SALES_HTTP_ALLOWED_HOSTS", "reports.example.test")
|
||||
monkeypatch.setenv("PCHOME_SALES_HTTP_BEARER_TOKEN", "top-secret-http-token")
|
||||
monkeypatch.setenv("PCHOME_SALES_IMAP_ENABLED", "true")
|
||||
monkeypatch.setenv("PCHOME_SALES_IMAP_HOST", "mail.example.test")
|
||||
monkeypatch.setenv("PCHOME_SALES_IMAP_USER", "private-user@example.test")
|
||||
monkeypatch.setenv("PCHOME_SALES_IMAP_PASSWORD", "top-secret-imap-password")
|
||||
|
||||
payload = acquisition.PChomeSalesAcquisitionService().readiness()
|
||||
public_text = json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
assert payload["manual_review_required"] is False
|
||||
assert payload["asset_coverage"]["ready"] == 3
|
||||
assert "top-secret" not in public_text
|
||||
assert "reports.example.test" not in public_text
|
||||
assert "mail.example.test" not in public_text
|
||||
assert "private-user" not in public_text
|
||||
|
||||
|
||||
def test_https_provider_rejects_non_https_and_non_allowlisted_hosts(monkeypatch):
|
||||
import services.pchome_sales_acquisition_service as acquisition
|
||||
import services.pchome_sales_acquisition_providers as provider_module
|
||||
|
||||
service = acquisition.PChomeSalesAcquisitionService()
|
||||
monkeypatch.setenv("PCHOME_SALES_HTTP_ALLOWED_HOSTS", "reports.example.test")
|
||||
|
||||
try:
|
||||
service.providers.validate_http_url("http://reports.example.test/report.xlsx")
|
||||
assert False, "HTTP URL should be rejected"
|
||||
except acquisition.ProviderError as exc:
|
||||
assert exc.kind == "http_url_rejected"
|
||||
|
||||
try:
|
||||
service.providers.validate_http_url("https://untrusted.example.test/report.xlsx")
|
||||
assert False, "Non-allowlisted host should be rejected"
|
||||
except acquisition.ProviderError as exc:
|
||||
assert exc.kind == "http_host_not_allowed"
|
||||
|
||||
monkeypatch.setenv("PCHOME_SALES_HTTP_ALLOWED_HOSTS", "reports.example.test")
|
||||
try:
|
||||
service.providers.validate_http_url("https://reports.example.test:8443/report.xlsx")
|
||||
assert False, "Non-allowlisted port should be rejected"
|
||||
except acquisition.ProviderError as exc:
|
||||
assert exc.kind == "http_port_rejected"
|
||||
|
||||
monkeypatch.setattr(
|
||||
provider_module.socket,
|
||||
"getaddrinfo",
|
||||
lambda *_args, **_kwargs: [(2, 1, 6, "", ("127.0.0.1", 443))],
|
||||
)
|
||||
try:
|
||||
service.providers.validate_http_url("https://reports.example.test/report.xlsx")
|
||||
assert False, "Private resolved address should be rejected"
|
||||
except acquisition.ProviderError as exc:
|
||||
assert exc.kind == "http_private_address_rejected"
|
||||
|
||||
|
||||
def test_auto_import_api_uses_governed_acquisition_service(monkeypatch):
|
||||
import routes.auto_import_routes as routes
|
||||
|
||||
class FakeAcquisitionService:
|
||||
def readiness(self):
|
||||
return {"success": True, "runtime_closure": "blocked_by_upstream_freshness"}
|
||||
|
||||
def run(self, *, trigger):
|
||||
return {
|
||||
"success": False,
|
||||
"status": "blocked_with_safe_next_action",
|
||||
"trigger": trigger,
|
||||
"receipt_persisted": True,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(routes, "pchome_sales_acquisition_service", FakeAcquisitionService())
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(routes.auto_import_bp)
|
||||
client = app.test_client()
|
||||
|
||||
readiness = client.get("/api/auto_import/readiness")
|
||||
run = client.post("/api/auto_import/run")
|
||||
legacy = client.post("/api/manual_import")
|
||||
|
||||
assert readiness.status_code == 200
|
||||
assert readiness.get_json()["runtime_closure"] == "blocked_by_upstream_freshness"
|
||||
assert run.get_json()["status"] == "blocked_with_safe_next_action"
|
||||
assert run.get_json()["trigger"] == "operator_command"
|
||||
assert legacy.get_json()["trigger"] == "operator_command"
|
||||
|
||||
|
||||
def test_scheduler_continues_when_legacy_hitl_pause_is_set(monkeypatch):
|
||||
import scheduler
|
||||
import services.agent_actions as agent_actions
|
||||
import services.pchome_sales_acquisition_service as acquisition
|
||||
|
||||
calls = []
|
||||
saved = {}
|
||||
monkeypatch.setattr(agent_actions, "is_task_paused", lambda _name: True)
|
||||
monkeypatch.setattr(
|
||||
acquisition.pchome_sales_acquisition_service,
|
||||
"run",
|
||||
lambda *, trigger: calls.append(trigger) or {
|
||||
"success": False,
|
||||
"status": "blocked_with_safe_next_action",
|
||||
"message": "no candidate",
|
||||
"trace_id": "trace-test",
|
||||
"receipt_id": "receipt-test",
|
||||
"receipt_persisted": True,
|
||||
"latest_sales_date": "2026-07-10",
|
||||
"data_lag_days": 1,
|
||||
"decision_ready": True,
|
||||
"safe_next_action": "scheduler_retry_authorized_sources",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(scheduler, "_save_stats", lambda name, stats: saved.update({"name": name, "stats": stats}))
|
||||
|
||||
scheduler.run_auto_import_task()
|
||||
|
||||
assert calls == ["scheduler"]
|
||||
assert saved["name"] == "auto_import_task"
|
||||
assert saved["stats"]["status"] == "Degraded"
|
||||
assert saved["stats"]["receipt_persisted"] is True
|
||||
Reference in New Issue
Block a user