"""Tier-specific tests: Lite tier feature set + gating. Lite unlocks exactly three tools — Deduplicator, Text Cleaner, Format Standardizer — 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")