Files
datatools-dev/tests/gui/test_activation.py
Michael db5ec084da docs+code: rename tool labels everywhere
Sweep follow-up to 93e43fc. Display labels now consistent across docs,
landing pages, CLI output, code comments, docstrings, and test prose.
Five parallel surfaces touched:

- docs (EN + ES): README, USER-GUIDE, CLI-REFERENCE, and 11 internal
  design/planning docs
- landing pages: index + bookkeeper/revops/shopify-pet
- src: CLI module docstrings, _TOOL_DISPLAY dicts in cli_analyze.py
  and gui/components/_legacy.py, core module headers, every tool
  page's module docstring
- tests: class/method/module docstrings and section-header comments
- test-cases READMEs

Page slugs (1_Deduplicator etc.), tool_id strings (01_deduplicator
etc.), Python class names (TestDeduplicatorWorkflow, FeatureFlag.*),
URL paths, anchor IDs, CSS classes, and asset filenames were left
intact since they're code identifiers / structural references.

All 2033 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 19:50:09 +00:00

252 lines
9.5 KiB
Python

"""GUI activation + license-gate tests.
These exercise the chrome-level gate that ``hide_streamlit_chrome``
installs: when no valid license is on disk, every page renders the
activation form instead of the page body, and tool widgets do NOT
appear. We test against the Find Duplicates page since it's the smallest
real-world tool that depends on chrome.
The autouse fixture in ``tests/conftest.py`` sets
``DATATOOLS_DEV_MODE=1``, which the GUI gate respects. Each test
below uses ``monkeypatch`` to clear that env var so the real gate
fires; ``isolated_license_path`` then redirects the manager to a
tmp file.
"""
from __future__ import annotations
import pytest
from streamlit.testing.v1 import AppTest
from .conftest import collected_text, stash_upload, with_language
@pytest.fixture
def no_license_env(monkeypatch, tmp_path):
"""Clear dev mode and point the license at a fresh empty tmp path."""
monkeypatch.delenv("DATATOOLS_DEV_MODE", raising=False)
monkeypatch.setenv("DATATOOLS_LICENSE_PATH", str(tmp_path / "license.json"))
from src.license.manager import reset_singleton_for_tests
reset_singleton_for_tests()
yield tmp_path / "license.json"
reset_singleton_for_tests()
@pytest.fixture
def trial_license(no_license_env):
"""Pre-activate a 1-year trial license for tests that need to
pass the gate."""
from src.license import LicenseManager, Tier
mgr = LicenseManager()
mgr.issue_trial(name="Test User", email="test@example.com")
yield mgr
class TestGateBlocksWithoutLicense:
"""When no license file exists, every page should render the
activation form and short-circuit the tool body."""
def test_home_renders_activation_form(self, no_license_env, home_app):
home_app.run()
text = collected_text(home_app)
assert "Activate DataTools" in text or "Activar DataTools" in text
def test_dedup_page_does_not_render_tool_widgets(
self, no_license_env, app_factory,
):
app = app_factory("1_Deduplicator")
app.run()
# Without a license, the page should NOT have the dedup-
# specific advanced-options expander or Find Duplicates button.
labels = [b.label for b in app.button]
assert not any("Find Duplicates" in lbl for lbl in labels), (
f"tool widgets leaked past the gate; got: {labels}"
)
def test_activation_form_localizes_to_spanish(
self, no_license_env, home_app,
):
with_language(home_app, "es")
home_app.run()
text = collected_text(home_app)
assert "Activar DataTools" in text
def test_sidebar_shows_not_activated(self, no_license_env, home_app):
home_app.run()
# Sidebar caption "🔒 Not activated".
captions = [c.value for c in home_app.sidebar.caption]
joined = " ".join(captions)
assert "Not activated" in joined or "Sin activar" in joined
class TestGatePassesWithTrialLicense:
def test_home_renders_full_grid(self, trial_license, home_app):
home_app.run()
text = collected_text(home_app)
# With a valid license, the activation form should NOT be the
# primary content; we should see the home title + tool cards.
assert "Data Cleaning Mastery" in text
assert "Activate DataTools" not in text # form not shown inline
def test_sidebar_shows_active_status(self, trial_license, home_app):
home_app.run()
captions = " ".join(c.value for c in home_app.sidebar.caption)
# "Trial · 364 days left" (give or take one).
assert "Trial" in captions or "Prueba" in captions
assert "days left" in captions or "días" in captions
def test_dedup_page_renders_tool_widgets(
self, trial_license, app_factory, small_csv_bytes,
):
app = app_factory("1_Deduplicator")
stash_upload(app, name="messy.csv", data=small_csv_bytes)
app.run()
labels = [b.label for b in app.button]
assert any("Find Duplicates" in lbl for lbl in labels), (
f"tool widgets blocked by gate even with valid license; "
f"got: {labels}"
)
class TestActivationFormSubmission:
"""End-to-end: paste a generated blob into the inline form and
confirm the gate releases on next render."""
def test_paste_blob_activates_and_unlocks(
self, no_license_env, home_app,
):
# Generate a blob the same way scripts/generate_license.py does.
from src.license import LicenseManager, Tier
from src.license.crypto import encode_blob
mint_mgr = LicenseManager()
# Use a separate tmp path so the trial we mint doesn't fight
# with the manager the GUI will use.
from tempfile import mkstemp
from pathlib import Path
_, p = mkstemp(suffix=".json")
mint_mgr._path = Path(p)
lic = mint_mgr._mint(
name="Buyer", email="buyer@example.com", tier=Tier.CORE,
)
blob = encode_blob(lic.to_dict())
# Wipe the temporary mint manager so its file doesn't collide.
mint_mgr.deactivate()
# Now drive the real GUI manager.
home_app.run()
# The activation form is inline in chrome — its widget keys
# are prefixed ``gate_``.
home_app.text_input(key="gate_name").set_value("Buyer").run()
home_app.text_input(key="gate_email").set_value("buyer@example.com").run()
home_app.text_area(key="gate_blob").set_value(blob).run()
# Submit primary form button.
submit = next(
b for b in home_app.button
if b.label in ("Activate", "Apply renewal")
)
submit.click().run()
# After activation the page reruns and the activation form
# should be gone — we should see the home page proper.
text = collected_text(home_app)
assert "Data Cleaning Mastery" in text
def test_trial_button_absent_paid_only(self, no_license_env, home_app):
"""v1.6 dropped the user-facing trial flow — paid licenses only.
Verify the trial button is not on the activation form."""
home_app.run()
labels = [b.label for b in home_app.button]
for lbl in labels:
assert "trial" not in lbl.lower(), (
f"trial button leaked into activation form: {lbl!r}"
)
assert "prueba" not in lbl.lower(), (
f"Spanish trial button leaked into form: {lbl!r}"
)
class TestActivationPageDirect:
"""``pages/_Activate.py`` renders the same form regardless of
license state — buyer can revisit it to review or deactivate."""
def test_activate_page_renders_with_valid_license(
self, trial_license, app_factory,
):
app = app_factory("_Activate")
app.run()
text = collected_text(app)
# Page title localized.
assert "Activate DataTools" in text
# Deactivate option only shown after activation.
labels = [b.label for b in app.button]
assert any("Deactivate" in lbl for lbl in labels)
def test_activate_page_renders_without_license(
self, no_license_env, app_factory,
):
app = app_factory("_Activate")
app.run()
text = collected_text(app)
assert "Activate DataTools" in text
# Deactivate button should NOT appear when nothing is active.
labels = [b.label for b in app.button]
assert not any("Deactivate" in lbl for lbl in labels)
class TestSidebarRenewalWarning:
"""A license with <30 days remaining surfaces a sidebar warning."""
def test_renewal_warning_appears_under_30_days(
self, no_license_env, home_app, monkeypatch,
):
# Mint a license with 7 days left.
from datetime import datetime, timedelta, timezone
from src.license import License, LicenseManager, Tier
from src.license import crypto as _crypto
from src.license.features import all_features_for_tier
future = (datetime.now(timezone.utc) + timedelta(days=7)).strftime(
"%Y-%m-%dT%H:%M:%SZ"
)
lic = License(
name="X", email="x@x.com",
license_key="DT1-CORE-EXPIRING",
tier=Tier.CORE,
features=all_features_for_tier(Tier.CORE),
issued_at="2026-05-13T00:00:00Z",
expires_at=future,
signature="",
)
sig = _crypto.sign(lic.to_canonical_dict())
signed = License(**{**lic.__dict__, "signature": sig})
LicenseManager().save(signed)
home_app.run()
sidebar_warnings = [w.body for w in home_app.sidebar.warning if w.body]
joined = " ".join(sidebar_warnings)
assert "expires in" in joined.lower() or "caduca en" in joined.lower(), (
f"expected renewal warning in sidebar; got: {sidebar_warnings}"
)
class TestLicenseStatusBadgeI18n:
"""Sidebar status badge tier name must localize."""
def test_core_tier_localizes_in_spanish(
self, no_license_env, home_app, monkeypatch,
):
from src.license import LicenseManager, Tier
LicenseManager()._mint(
name="X", email="x@x.com", tier=Tier.CORE,
)
with_language(home_app, "es")
home_app.run()
captions = " ".join(c.value for c in home_app.sidebar.caption)
# The es pack maps ``license.tier_core`` to "Core" — same word
# in Spanish — but the surrounding template (``días restantes``)
# localizes.
assert "días restantes" in captions, (
f"Spanish status label missing; sidebar captions: {captions}"
)