"""Unit tests for the PAT Registry module.""" from __future__ import annotations import json from pathlib import Path import pytest from scripts.pat_manager.errors import RegistryValidationError from scripts.pat_manager.models import PatEntry from scripts.pat_manager.registry import PatRegistry @pytest.fixture def registry() -> PatRegistry: """Create a fresh PatRegistry instance.""" return PatRegistry() @pytest.fixture def valid_entry_dict() -> dict: """A valid PAT entry dictionary.""" return { "service": "GitLab", "token_name": "ci-pipeline-token", "expiry_date": "2025-08-15", "renewal_method": "auto", } @pytest.fixture def valid_registry_data() -> dict: """A valid registry JSON structure with multiple entries.""" return { "version": "1.0", "tokens": [ { "service": "GitLab", "token_name": "ci-pipeline-token", "expiry_date": "2025-08-15", "renewal_method": "auto", }, { "service": "Jira", "token_name": "jira-api-access", "expiry_date": "2025-07-20", "renewal_method": "manual", }, { "service": "OrgMyLife", "token_name": "orchestrator-api-key", "expiry_date": None, "renewal_method": "manual", }, ], } class TestValidateEntry: """Tests for PatRegistry.validate_entry().""" def test_valid_gitlab_entry(self, registry: PatRegistry, valid_entry_dict: dict) -> None: result = registry.validate_entry(valid_entry_dict) assert isinstance(result, PatEntry) assert result.service == "GitLab" assert result.token_name == "ci-pipeline-token" assert result.expiry_date == "2025-08-15" assert result.renewal_method == "auto" def test_valid_jira_entry(self, registry: PatRegistry) -> None: entry = { "service": "Jira", "token_name": "jira-token", "expiry_date": "2025-12-31", "renewal_method": "manual", } result = registry.validate_entry(entry) assert result.service == "Jira" assert result.renewal_method == "manual" def test_valid_confluence_entry(self, registry: PatRegistry) -> None: entry = { "service": "Confluence", "token_name": "conf-token", "expiry_date": "2025-06-01", "renewal_method": "manual", } result = registry.validate_entry(entry) assert result.service == "Confluence" def test_valid_orgmylife_entry(self, registry: PatRegistry) -> None: entry = { "service": "OrgMyLife", "token_name": "oml-key", "expiry_date": None, "renewal_method": "manual", } result = registry.validate_entry(entry) assert result.service == "OrgMyLife" assert result.expiry_date is None def test_null_expiry_date_accepted(self, registry: PatRegistry) -> None: entry = { "service": "Jira", "token_name": "no-expiry", "expiry_date": None, "renewal_method": "manual", } result = registry.validate_entry(entry) assert result.expiry_date is None def test_missing_expiry_date_field_accepted(self, registry: PatRegistry) -> None: """expiry_date is optional (defaults to None when missing).""" entry = { "service": "Jira", "token_name": "no-expiry-field", "renewal_method": "manual", } result = registry.validate_entry(entry) assert result.expiry_date is None def test_missing_service_field(self, registry: PatRegistry) -> None: entry = {"token_name": "test", "renewal_method": "manual"} with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "service" in exc_info.value.failed_fields def test_missing_token_name_field(self, registry: PatRegistry) -> None: entry = {"service": "GitLab", "renewal_method": "auto"} with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "token_name" in exc_info.value.failed_fields def test_missing_renewal_method_field(self, registry: PatRegistry) -> None: entry = {"service": "GitLab", "token_name": "test"} with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "renewal_method" in exc_info.value.failed_fields def test_invalid_service_value(self, registry: PatRegistry) -> None: entry = { "service": "GitHub", "token_name": "test", "renewal_method": "manual", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "service" in exc_info.value.failed_fields def test_empty_token_name(self, registry: PatRegistry) -> None: entry = { "service": "GitLab", "token_name": "", "renewal_method": "auto", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "token_name" in exc_info.value.failed_fields def test_token_name_too_long(self, registry: PatRegistry) -> None: entry = { "service": "GitLab", "token_name": "x" * 129, "renewal_method": "auto", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "token_name" in exc_info.value.failed_fields def test_token_name_max_length_accepted(self, registry: PatRegistry) -> None: entry = { "service": "GitLab", "token_name": "x" * 128, "expiry_date": "2025-01-01", "renewal_method": "auto", } result = registry.validate_entry(entry) assert len(result.token_name) == 128 def test_invalid_expiry_date_format(self, registry: PatRegistry) -> None: entry = { "service": "GitLab", "token_name": "test", "expiry_date": "2025/08/15", "renewal_method": "auto", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "expiry_date" in exc_info.value.failed_fields def test_invalid_expiry_date_values(self, registry: PatRegistry) -> None: entry = { "service": "GitLab", "token_name": "test", "expiry_date": "2025-13-01", "renewal_method": "auto", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "expiry_date" in exc_info.value.failed_fields def test_invalid_expiry_date_feb_30(self, registry: PatRegistry) -> None: entry = { "service": "GitLab", "token_name": "test", "expiry_date": "2025-02-30", "renewal_method": "auto", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "expiry_date" in exc_info.value.failed_fields def test_renewal_method_mismatch_gitlab(self, registry: PatRegistry) -> None: """GitLab must have renewal_method 'auto'.""" entry = { "service": "GitLab", "token_name": "test", "expiry_date": "2025-01-01", "renewal_method": "manual", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "renewal_method" in exc_info.value.failed_fields def test_renewal_method_mismatch_jira(self, registry: PatRegistry) -> None: """Jira must have renewal_method 'manual'.""" entry = { "service": "Jira", "token_name": "test", "expiry_date": "2025-01-01", "renewal_method": "auto", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert "renewal_method" in exc_info.value.failed_fields def test_non_dict_entry_raises(self, registry: PatRegistry) -> None: with pytest.raises(RegistryValidationError): registry.validate_entry("not a dict") # type: ignore def test_multiple_failed_fields(self, registry: PatRegistry) -> None: entry = { "service": "InvalidService", "token_name": "", "renewal_method": "invalid", } with pytest.raises(RegistryValidationError) as exc_info: registry.validate_entry(entry) assert len(exc_info.value.failed_fields) >= 2 class TestLoad: """Tests for PatRegistry.load().""" def test_load_valid_registry( self, registry: PatRegistry, tmp_path: Path, valid_registry_data: dict ) -> None: file_path = tmp_path / "registry.json" file_path.write_text(json.dumps(valid_registry_data), encoding="utf-8") entries = registry.load(file_path) assert len(entries) == 3 assert entries[0].service == "GitLab" assert entries[1].service == "Jira" assert entries[2].service == "OrgMyLife" def test_load_empty_registry(self, registry: PatRegistry, tmp_path: Path) -> None: file_path = tmp_path / "registry.json" file_path.write_text('{"version": "1.0", "tokens": []}', encoding="utf-8") entries = registry.load(file_path) assert entries == [] def test_load_nonexistent_file(self, registry: PatRegistry, tmp_path: Path) -> None: file_path = tmp_path / "nonexistent.json" with pytest.raises(FileNotFoundError): registry.load(file_path) def test_load_invalid_json(self, registry: PatRegistry, tmp_path: Path) -> None: file_path = tmp_path / "registry.json" file_path.write_text("not valid json", encoding="utf-8") with pytest.raises(json.JSONDecodeError): registry.load(file_path) def test_load_duplicate_entries_raises( self, registry: PatRegistry, tmp_path: Path ) -> None: data = { "version": "1.0", "tokens": [ { "service": "GitLab", "token_name": "same-name", "expiry_date": "2025-01-01", "renewal_method": "auto", }, { "service": "GitLab", "token_name": "same-name", "expiry_date": "2025-06-01", "renewal_method": "auto", }, ], } file_path = tmp_path / "registry.json" file_path.write_text(json.dumps(data), encoding="utf-8") with pytest.raises(RegistryValidationError) as exc_info: registry.load(file_path) assert "service" in exc_info.value.failed_fields assert "token_name" in exc_info.value.failed_fields def test_load_same_token_name_different_services( self, registry: PatRegistry, tmp_path: Path ) -> None: """Same token_name in different services is allowed.""" data = { "version": "1.0", "tokens": [ { "service": "GitLab", "token_name": "api-token", "expiry_date": "2025-01-01", "renewal_method": "auto", }, { "service": "Jira", "token_name": "api-token", "expiry_date": "2025-06-01", "renewal_method": "manual", }, ], } file_path = tmp_path / "registry.json" file_path.write_text(json.dumps(data), encoding="utf-8") entries = registry.load(file_path) assert len(entries) == 2 def test_load_invalid_entry_raises( self, registry: PatRegistry, tmp_path: Path ) -> None: data = { "version": "1.0", "tokens": [ { "service": "InvalidService", "token_name": "test", "renewal_method": "manual", } ], } file_path = tmp_path / "registry.json" file_path.write_text(json.dumps(data), encoding="utf-8") with pytest.raises(RegistryValidationError): registry.load(file_path) class TestSave: """Tests for PatRegistry.save().""" def test_save_entries(self, registry: PatRegistry, tmp_path: Path) -> None: entries = [ PatEntry( service="GitLab", token_name="test-token", expiry_date="2025-08-15", renewal_method="auto", ), PatEntry( service="Jira", token_name="jira-token", expiry_date=None, renewal_method="manual", ), ] file_path = tmp_path / "registry.json" registry.save(file_path, entries) with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) assert data["version"] == "1.0" assert len(data["tokens"]) == 2 assert data["tokens"][0]["service"] == "GitLab" assert data["tokens"][1]["expiry_date"] is None def test_save_empty_list(self, registry: PatRegistry, tmp_path: Path) -> None: file_path = tmp_path / "registry.json" registry.save(file_path, []) with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) assert data == {"version": "1.0", "tokens": []} def test_save_creates_parent_directories( self, registry: PatRegistry, tmp_path: Path ) -> None: file_path = tmp_path / "subdir" / "nested" / "registry.json" registry.save(file_path, []) assert file_path.exists() class TestCreateEmpty: """Tests for PatRegistry.create_empty().""" def test_create_empty_file(self, registry: PatRegistry, tmp_path: Path) -> None: file_path = tmp_path / "new-registry.json" registry.create_empty(file_path) assert file_path.exists() with open(file_path, "r", encoding="utf-8") as f: data = json.load(f) assert data == {"version": "1.0", "tokens": []} def test_create_empty_loadable(self, registry: PatRegistry, tmp_path: Path) -> None: """An empty registry created by create_empty should be loadable.""" file_path = tmp_path / "new-registry.json" registry.create_empty(file_path) entries = registry.load(file_path) assert entries == [] class TestRoundTrip: """Tests for save -> load round-trip consistency.""" def test_round_trip_preserves_entries( self, registry: PatRegistry, tmp_path: Path ) -> None: original_entries = [ PatEntry( service="GitLab", token_name="ci-token", expiry_date="2025-08-15", renewal_method="auto", ), PatEntry( service="Jira", token_name="jira-access", expiry_date="2025-12-31", renewal_method="manual", ), PatEntry( service="OrgMyLife", token_name="api-key", expiry_date=None, renewal_method="manual", ), ] file_path = tmp_path / "registry.json" registry.save(file_path, original_entries) loaded_entries = registry.load(file_path) assert len(loaded_entries) == len(original_entries) for original, loaded in zip(original_entries, loaded_entries): assert loaded.service == original.service assert loaded.token_name == original.token_name assert loaded.expiry_date == original.expiry_date assert loaded.renewal_method == original.renewal_method