from datetime import date as Date import pydantic import pytest from rendercv.renderer.templater.model_processor import process_fields, process_model from rendercv.schema.models.cv.cv import Cv from rendercv.schema.models.cv.entries.normal import NormalEntry from rendercv.schema.models.rendercv_model import RenderCVModel @pytest.fixture def recorder(): """Return a processor function and a list tracking all values it has processed. Used to verify which fields get processed and in what order. """ seen = [] def fn(v: str) -> str: seen.append(v) return f"processed-{v}" return fn, seen class TestProcessFields: def test_applies_processors_in_order(self): processors = [lambda s: s.upper(), lambda s: f"{s}!"] result = process_fields("content", processors) assert result == "CONTENT!" def test_processes_fields_and_mutates_entry(self, recorder): fn, seen = recorder entry = NormalEntry.model_validate( { "name": "Entry", "summary": "hello", "highlights": ["a", "b"], "start_date": "2020-01-01", "end_date": "2020-02-01", "location": "Remote", } ) process_fields(entry, [fn]) assert "hello" in seen assert "a" in seen assert "b" in seen assert "Remote" in seen assert "2020-01-01" not in seen assert "2020-02-01" not in seen assert entry.summary == "processed-hello" assert entry.highlights == ["processed-a", "processed-b"] assert entry.location == "processed-Remote" assert entry.start_date == "2020-01-01" assert entry.end_date == "2020-02-01" def test_converts_non_string_non_list_fields_to_string(self, recorder): class EntryWithInt(pydantic.BaseModel): name: str count: int fn, seen = recorder entry = EntryWithInt(name="Test", count=42) process_fields(entry, [fn]) # ty: ignore[invalid-argument-type] assert "Test" in seen assert "42" in seen assert entry.name == "processed-Test" assert entry.count == "processed-42" @pytest.fixture(params=[["Python", "Remote"], []]) def model(request: pytest.FixtureRequest) -> RenderCVModel: """Return a test RenderCVModel with keywords either set or empty. Parametrized to test both with and without bold keywords. """ cv_data = { # Order matters for connections "name": "Jane Doe @", "headline": "Software Engineer @", "email": "jane@example.com", "website": "https://janedoe.dev", "sections": { "Professional Experience": [ { "name": "Backend Work", "summary": "Built Python services with *markdown* emphasis.", "highlights": ["Improved Python performance"], "start_date": "2022-01-01", "end_date": "2023-02-01", "location": "Remote", } ] }, } cv = Cv.model_validate(cv_data) rendercv_model = RenderCVModel(cv=cv) rendercv_model.settings.current_date = Date(2024, 2, 1) rendercv_model.settings.bold_keywords = request.param return rendercv_model class TestProcessModel: def test_markdown_output_has_correct_structure(self, model): result = process_model(model, "markdown") assert result.cv.name == "Jane Doe @" assert result.cv.headline == "Software Engineer @" # Connections and last updated date are added to cv assert result.cv.connections == [ # ty: ignore[unresolved-attribute] "[jane@example.com](mailto:jane@example.com)", "[janedoe.dev](https://janedoe.dev/)", ] assert ( result.cv.top_note == "*Last updated in Feb 2024*" # ty: ignore[unresolved-attribute] ) entry = result.cv.rendercv_sections[0].entries[0] assert entry.main_column.startswith("**Backend Work**") if model.settings.bold_keywords: assert ( "Built **Python** services with *markdown* emphasis." in entry.main_column ) assert "- Improved **Python** performance" in entry.main_column # DATE placeholder removed because it's not provided; location remains assert entry.date_and_location_column == "**Remote**\nJan 2022 – Feb 2023" else: assert ( "Built Python services with *markdown* emphasis." in entry.main_column ) assert "- Improved Python performance" in entry.main_column assert entry.date_and_location_column == "Remote\nJan 2022 – Feb 2023" def test_typst_output_escapes_special_characters(self, model): result = process_model(model, "typst") assert result.cv.name == "Jane Doe \\@" assert result.cv.headline == "Software Engineer \\@" entry = result.cv.rendercv_sections[0].entries[0] assert entry.main_column.startswith("#strong[Backend Work]") if model.settings.bold_keywords: assert "- Improved #strong[Python] performance" in entry.main_column assert ( entry.date_and_location_column == "#strong[Remote]\nJan 2022 – Feb 2023" ) # Connections rendered as Typst links with icons by default assert result.cv.connections[0].startswith("#link(") # ty: ignore[unresolved-attribute] assert "#connection-with-icon" in result.cv.connections[0] # ty: ignore[unresolved-attribute] else: assert "- Improved Python performance" in entry.main_column assert entry.date_and_location_column == "Remote\nJan 2022 – Feb 2023" assert result.cv.connections[0].startswith("#link(") # ty: ignore[unresolved-attribute] assert "jane@example.com" in result.cv.connections[0] # ty: ignore[unresolved-attribute] def test_handles_cv_with_no_sections(self): cv_data = { "name": "Jane Doe", "headline": "Software Engineer", } cv = Cv.model_validate(cv_data) rendercv_model = RenderCVModel(cv=cv) result = process_model(rendercv_model, "markdown") assert result.cv.name == "Jane Doe" assert result.cv.headline == "Software Engineer" assert hasattr(result.cv, "connections")