"""ORCH-069: unit tests for the configurable QG-0 title-length limit. Covers `_qg0_errors` (src/webhooks/plane.py) reading the upper title limit dynamically from `settings.qg0_title_max` (env `ORCH_QG0_TITLE_MAX`, default 200), plus the graceful env-degradation field-validator on `Settings`. The tests patch `src.config.settings.qg0_title_max` (the same object imported into `src.webhooks.plane`) and assert boundary behaviour and error texts. For env-driven cases a FRESH `Settings()` instance is created locally, since the module-level singleton is built once on import. """ import re import pytest from src.config import Settings, settings from src.webhooks.plane import _qg0_errors VALID_DESCRIPTION = "x" * 30 # >= 20 chars, always passes the description check def _title_length_error(errors): """Return the title length-limit error string, or None if absent. The short-title error ('нужно >= 5') and the description error are excluded; only the 'too long' title error is matched (it contains 'максимум'). """ for e in errors: if "Title" in e and "максимум" in e: return e return None # --- AC-1: default limit 200, boundary at 201 ------------------------------ def test_tc01_default_limit_200_boundary_pass(monkeypatch): """TC-01: title of exactly 200 chars -> no title length error (PASS).""" monkeypatch.setattr(settings, "qg0_title_max", 200) errors = _qg0_errors("x" * 200, VALID_DESCRIPTION) assert _title_length_error(errors) is None def test_tc02_default_limit_200_boundary_fail(monkeypatch): """TC-02: title of 201 chars -> length error mentioning '200'.""" monkeypatch.setattr(settings, "qg0_title_max", 200) errors = _qg0_errors("x" * 201, VALID_DESCRIPTION) err = _title_length_error(errors) assert err is not None assert "200" in err # --- AC-2: configurable limit 120, boundary at 121 ------------------------- def test_tc03_custom_limit_120_boundary_pass(monkeypatch): """TC-03: with limit 120, a 120-char title passes.""" monkeypatch.setattr(settings, "qg0_title_max", 120) errors = _qg0_errors("x" * 120, VALID_DESCRIPTION) assert _title_length_error(errors) is None def test_tc04_custom_limit_120_boundary_fail(monkeypatch): """TC-04: with limit 120, a 121-char title fails; text mentions 120 not 80.""" monkeypatch.setattr(settings, "qg0_title_max", 120) errors = _qg0_errors("x" * 121, VALID_DESCRIPTION) err = _title_length_error(errors) assert err is not None assert "120" in err assert "80" not in err # --- AC-3: graceful handling of invalid/empty env -------------------------- def test_tc05_graceful_non_numeric_env(monkeypatch): """TC-05: non-numeric env -> Settings() does not raise, limit == 200.""" monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "abc") s = Settings() assert s.qg0_title_max == 200 def test_tc06_graceful_empty_env(monkeypatch): """TC-06: empty-string env -> default 200, no exception.""" monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "") s = Settings() assert s.qg0_title_max == 200 def test_tc07_valid_numeric_env(monkeypatch): """TC-07: valid numeric env -> the given value is applied (positive path).""" monkeypatch.setenv("ORCH_QG0_TITLE_MAX", "150") s = Settings() assert s.qg0_title_max == 150 # --- AC-4: lower limits unchanged ------------------------------------------ def test_tc08_short_title_still_errors(monkeypatch): """TC-08: title < 5 chars still raises the short-title error.""" monkeypatch.setattr(settings, "qg0_title_max", 200) errors = _qg0_errors("abc", VALID_DESCRIPTION) assert any("Title" in e and "нужно >= 5" in e for e in errors) def test_tc09_short_description_still_errors(monkeypatch): """TC-09: description < 20 chars still raises the short-description error.""" monkeypatch.setattr(settings, "qg0_title_max", 200) errors = _qg0_errors("Valid title", "short") assert any("Description" in e for e in errors) # --- AC-7: backward compatibility ------------------------------------------ def test_tc10_backward_compat_titles_81_to_200(monkeypatch): """TC-10: a title previously rejected by the 80-char cap now passes at 200.""" monkeypatch.setattr(settings, "qg0_title_max", 200) errors = _qg0_errors("x" * 100, VALID_DESCRIPTION) assert _title_length_error(errors) is None