Files
datatools-dev/tests/test_lite_tier.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

151 lines
5.5 KiB
Python

"""Tier-specific tests: Lite tier feature set + gating.
Lite unlocks exactly three tools — Find Duplicates, Clean Text,
Standardize Formats — and locks the other six. We test:
- The features map for Lite returns the right three flags (and only
those three).
- A Lite-tier license passes ``require_feature`` for any of the three
unlocked flags and raises ``UnsupportedFeatureError`` for any
locked flag.
- The CLI guard exits with code 2 when a Lite user runs a non-Lite
tool CLI.
GUI behaviour is covered in ``tests/gui/test_lite_tier.py`` so the
GUI/non-GUI split stays clean.
"""
from __future__ import annotations
import pytest
from src.license import (
FeatureFlag,
LicenseManager,
Tier,
UnsupportedFeatureError,
)
from src.license.features import FEATURES_BY_TIER, all_features_for_tier
# ---------------------------------------------------------------------------
# Features map
# ---------------------------------------------------------------------------
class TestLiteFeatureMap:
def test_lite_unlocks_exactly_three_tools(self):
flags = FEATURES_BY_TIER[Tier.LITE]
assert flags == frozenset({
FeatureFlag.DEDUPLICATOR,
FeatureFlag.TEXT_CLEANER,
FeatureFlag.FORMAT_STANDARDIZER,
})
def test_lite_locks_other_six_tools(self):
flags = FEATURES_BY_TIER[Tier.LITE]
locked = {
FeatureFlag.MISSING_HANDLER,
FeatureFlag.COLUMN_MAPPER,
FeatureFlag.OUTLIER_DETECTOR,
FeatureFlag.MULTI_FILE_MERGER,
FeatureFlag.VALIDATOR_REPORTER,
FeatureFlag.PIPELINE_RUNNER,
}
for f in locked:
assert f not in flags
def test_all_features_for_tier_lite_returns_sorted_tuple(self):
tup = all_features_for_tier(Tier.LITE)
assert tup == tuple(sorted(tup))
assert len(tup) == 3
def test_core_still_unlocks_every_tool(self):
"""Sanity: adding Lite didn't accidentally narrow Core."""
assert FEATURES_BY_TIER[Tier.CORE] == frozenset(FeatureFlag)
# ---------------------------------------------------------------------------
# Manager gating
# ---------------------------------------------------------------------------
class TestLiteLicenseGating:
@pytest.fixture
def lite_manager(self, isolated_license_path):
"""Pre-activate a Lite license and return the manager."""
mgr = LicenseManager()
mgr._mint(name="Lite User", email="lite@example.com", tier=Tier.LITE)
return mgr
@pytest.mark.parametrize("feature", [
FeatureFlag.DEDUPLICATOR,
FeatureFlag.TEXT_CLEANER,
FeatureFlag.FORMAT_STANDARDIZER,
])
def test_lite_passes_for_included_features(self, lite_manager, feature):
lite_manager.require_feature(feature) # no raise
@pytest.mark.parametrize("feature", [
FeatureFlag.MISSING_HANDLER,
FeatureFlag.COLUMN_MAPPER,
FeatureFlag.OUTLIER_DETECTOR,
FeatureFlag.MULTI_FILE_MERGER,
FeatureFlag.VALIDATOR_REPORTER,
FeatureFlag.PIPELINE_RUNNER,
])
def test_lite_raises_for_locked_features(self, lite_manager, feature):
with pytest.raises(UnsupportedFeatureError):
lite_manager.require_feature(feature)
def test_lite_state_carries_lite_tier(self, lite_manager):
state = lite_manager.current_state()
assert state.tier == "lite"
assert state.valid is True
# ---------------------------------------------------------------------------
# CLI guard
# ---------------------------------------------------------------------------
class TestLiteCliGuard:
"""In-process tests of the guard. Subprocess equivalents would be
slower; the unit-level coverage is sufficient because the guard
is pure Python and the entrypoint plumbing is already tested in
``test_license_cli.py``."""
def _activate_lite(self, monkeypatch, 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()
from src.license import LicenseManager
LicenseManager()._mint(
name="A", email="a@b.com", tier=Tier.LITE,
)
def test_guard_passes_for_unlocked_feature(self, monkeypatch, tmp_path):
self._activate_lite(monkeypatch, tmp_path)
monkeypatch.setattr("sys.argv", ["prog", "input.csv"])
from src.cli_license_guard import guard
# Lite unlocks text cleaner — guard must not raise.
guard(feature="02_text_cleaner")
def test_guard_rejects_locked_feature(self, monkeypatch, tmp_path, capsys):
self._activate_lite(monkeypatch, tmp_path)
monkeypatch.setattr("sys.argv", ["prog", "input.csv"])
from src.cli_license_guard import guard
with pytest.raises(SystemExit) as ei:
guard(feature="04_missing_handler")
assert ei.value.code == 2
captured = capsys.readouterr()
assert "04_missing_handler" in captured.err
assert "upgrade" in captured.err.lower() or "tier" in captured.err.lower()
def test_guard_help_bypasses_feature_check(self, monkeypatch, tmp_path):
self._activate_lite(monkeypatch, tmp_path)
monkeypatch.setattr("sys.argv", ["prog", "--help"])
from src.cli_license_guard import guard
# --help still bypasses the guard entirely.
guard(feature="04_missing_handler")