"""Tests for src/utils.py — filename generation, path building, redaction.""" from pathlib import Path import pytest from src.utils import ( build_export_path, format_token_status, generate_filename, redact_secrets, ) class TestGenerateFilename: def test_basic_format(self): name = generate_filename("Hello World", "abc12345def", "2024-06-10T14:00:00Z") assert name == "2024-06-10_hello-world_abc12345.md" def test_special_chars_slugified(self): # T-36: titles with punctuation must produce safe, OS-compatible filenames name = generate_filename("What's this?! A test.", "abc12345", "2024-06-01T00:00:00Z") assert "?" not in name assert "!" not in name assert "'" not in name assert " " not in name assert name.startswith("2024-06-01_") assert name.endswith("_abc12345.md") def test_unicode_chars_handled(self): name = generate_filename("Héllo Wörld", "abc12345", "2024-06-01T00:00:00Z") assert " " not in name assert name.endswith("_abc12345.md") def test_empty_title_becomes_untitled(self): name = generate_filename("", "abc12345", "2024-06-01T00:00:00Z") assert "untitled" in name def test_id_truncated_to_8_chars(self): name = generate_filename("Test", "abcdefghijklmnop", "2024-06-01T00:00:00Z") assert name.endswith("_abcdefgh.md") def test_long_title_truncated(self): long_title = "a" * 200 name = generate_filename(long_title, "abc12345", "2024-06-01T00:00:00Z") # Slug is capped at 60 chars by max_length slug_part = name.split("_")[1] assert len(slug_part) <= 60 def test_date_comes_from_created_at(self): name = generate_filename("Test", "abc12345", "2023-11-25T00:00:00Z") assert name.startswith("2023-11-25_") class TestBuildExportPath: def test_default_structure_provider_project_year(self): path = build_export_path( Path("/exports"), "claude", "my-project", "2024-06-01T00:00:00Z", "file.md" ) assert str(path) == "/exports/claude/my-project/2024/file.md" def test_no_project_uses_no_project_slug(self): path = build_export_path( Path("/exports"), "chatgpt", None, "2024-06-01T00:00:00Z", "file.md" ) assert "no-project" in str(path) def test_provider_project_structure_omits_year(self): path = build_export_path( Path("/exports"), "claude", "proj", "2024-06-01T00:00:00Z", "file.md", structure="provider/project", ) assert "2024" not in str(path) assert "proj" in str(path) def test_provider_year_structure_omits_project(self): path = build_export_path( Path("/exports"), "claude", "proj", "2024-06-01T00:00:00Z", "file.md", structure="provider/year", ) assert "proj" not in str(path) assert "2024" in str(path) def test_project_name_with_spaces_is_slugified(self): path = build_export_path( Path("/exports"), "claude", "My Project Name!", "2024-06-01T00:00:00Z", "file.md" ) assert " " not in str(path) assert "!" not in str(path) class TestRedactSecrets: def test_token_value_redacted(self): data = {"token": "supersecret"} result = redact_secrets(data) assert result["token"] == "[REDACTED]" def test_session_key_redacted(self): result = redact_secrets({"sessionKey": "abc123"}) assert result["sessionKey"] == "[REDACTED]" def test_non_sensitive_key_unchanged(self): result = redact_secrets({"title": "My Chat", "id": "abc123"}) assert result["title"] == "My Chat" assert result["id"] == "abc123" def test_nested_dict_redacted(self): data = {"user": {"token": "secret", "name": "Alice"}} result = redact_secrets(data) assert result["user"]["token"] == "[REDACTED]" assert result["user"]["name"] == "Alice" def test_list_of_dicts(self): data = [{"password": "p@ss"}, {"title": "chat"}] result = redact_secrets(data) assert result[0]["password"] == "[REDACTED]" assert result[1]["title"] == "chat" class TestFormatTokenStatus: def test_none_token_returns_not_set(self): assert format_token_status(None) == "[NOT SET]" def test_empty_token_returns_not_set(self): assert format_token_status("") == "[NOT SET]" def test_set_token_no_expiry(self): assert format_token_status("sometoken") == "[SET]" def test_expired_token(self): from datetime import datetime, timezone, timedelta expiry = datetime.now(tz=timezone.utc) - timedelta(days=1) result = format_token_status("tok", expiry) assert "EXPIRED" in result def test_expiring_today_shows_hours(self): from datetime import datetime, timezone, timedelta expiry = datetime.now(tz=timezone.utc) + timedelta(hours=3) result = format_token_status("tok", expiry) assert "expires in" in result assert "h" in result def test_expiring_in_days(self): from datetime import datetime, timezone, timedelta expiry = datetime.now(tz=timezone.utc) + timedelta(days=10, hours=12) result = format_token_status("tok", expiry) assert "10 days" in result