Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)
Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
466 lines
16 KiB
Python
466 lines
16 KiB
Python
"""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
|