148 lines
5.3 KiB
Python
148 lines
5.3 KiB
Python
"""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
|