diff --git a/app/wizard/step_integrations.py b/app/wizard/step_integrations.py new file mode 100644 index 0000000..a32839f --- /dev/null +++ b/app/wizard/step_integrations.py @@ -0,0 +1,36 @@ +"""Step 7 — Optional integrations (cloud storage, calendars, notifications). + +This step is never mandatory — validate() always returns []. +Helper functions support the wizard UI for tier-filtered integration cards. +""" +from __future__ import annotations +from pathlib import Path + + +def validate(data: dict) -> list[str]: + """Integrations step is optional — never blocks Finish.""" + return [] + + +def get_available(tier: str) -> list[str]: + """Return list of integration names available for the given tier. + + An integration is available if the user's tier meets or exceeds the + integration's minimum required tier (as declared by cls.tier). + """ + from scripts.integrations import REGISTRY + from app.wizard.tiers import TIERS + + available = [] + for name, cls in REGISTRY.items(): + try: + if TIERS.index(tier) >= TIERS.index(cls.tier): + available.append(name) + except ValueError: + pass # unknown tier string — skip + return available + + +def is_connected(name: str, config_dir: Path) -> bool: + """Return True if a live config file exists for this integration.""" + return (config_dir / "integrations" / f"{name}.yaml").exists() diff --git a/tests/test_wizard_steps.py b/tests/test_wizard_steps.py index 37b6a87..c227236 100644 --- a/tests/test_wizard_steps.py +++ b/tests/test_wizard_steps.py @@ -110,3 +110,35 @@ def test_search_missing_both(): def test_search_none_values(): d = {"job_titles": None, "locations": None} assert search_validate(d) != [] + +# ── Step Integrations ────────────────────────────────────────────────────────── +from app.wizard.step_integrations import validate as int_validate, get_available, is_connected + +def test_integrations_always_passes(): + assert int_validate({}) == [] + assert int_validate({"connected": ["notion", "slack"]}) == [] + +def test_get_available_free_tier_includes_free(): + available = get_available("free") + # Free integrations must always be available + for name in ["google_drive", "dropbox", "discord", "home_assistant"]: + assert name in available, f"{name} should be in free tier available list" + +def test_get_available_free_tier_excludes_paid(): + available = get_available("free") + # Paid integrations should NOT be available on free tier + for name in ["notion", "google_calendar", "slack"]: + assert name not in available, f"{name} should NOT be in free tier available list" + +def test_get_available_paid_tier_includes_paid(): + available = get_available("paid") + for name in ["notion", "google_sheets", "airtable", "slack", "google_calendar"]: + assert name in available, f"{name} should be in paid tier available list" + +def test_is_connected_false_when_no_file(tmp_path): + assert is_connected("notion", tmp_path) is False + +def test_is_connected_true_when_file_exists(tmp_path): + (tmp_path / "integrations").mkdir() + (tmp_path / "integrations" / "notion.yaml").write_text("token: x\n") + assert is_connected("notion", tmp_path) is True