d18c10996a
- Add release_names.json mapping version ranges to codenames - Add release_name property to Settings in app/config.py - Update build metadata script to include codename in RUNTIME_INFO - Display release codename in status dashboard and page footer - Inject release_name globally via template response wrapper - Update ROADMAP.md with codenames for all milestone releases - Add docs/ReleaseNaming.md with naming guide and best practices - Add comprehensive tests for release name resolution Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
268 lines
11 KiB
Python
268 lines
11 KiB
Python
"""Tests for release naming functionality in app/config.py and app/views/status.py."""
|
|
|
|
import json
|
|
import os
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestReleaseNameProperty:
|
|
"""Tests for the Settings.release_name property."""
|
|
|
|
def test_release_name_returns_codename_for_minor_prefix(self, tmp_path):
|
|
"""Test release_name returns codename matching minor version prefix."""
|
|
release_data = {
|
|
"releases": {
|
|
"0.5": {"codename": "Foundation", "description": "Core platform"},
|
|
}
|
|
}
|
|
release_file = tmp_path / "release_names.json"
|
|
release_file.write_text(json.dumps(release_data))
|
|
|
|
with (
|
|
patch("app.config.Settings.version", new_callable=lambda: property(lambda self: "0.5.3")),
|
|
patch("app.config.os.path.join", return_value=str(release_file)),
|
|
patch("app.config.os.path.exists", return_value=True),
|
|
):
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
# Directly call the property with mocked file path
|
|
with (
|
|
patch.object(type(s), "version", new_callable=lambda: property(lambda self: "0.5.3")),
|
|
patch("app.config.os.path.dirname"),
|
|
patch("app.config.os.path.join", return_value=str(release_file)),
|
|
patch("app.config.os.path.exists", return_value=True),
|
|
):
|
|
result = s.release_name
|
|
assert result == "Foundation"
|
|
|
|
def test_release_name_returns_codename_for_exact_match(self, tmp_path):
|
|
"""Test release_name returns codename for exact version match."""
|
|
release_data = {
|
|
"releases": {
|
|
"1.0.0": {"codename": "Summit"},
|
|
}
|
|
}
|
|
release_file = tmp_path / "release_names.json"
|
|
release_file.write_text(json.dumps(release_data))
|
|
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
with (
|
|
patch.object(type(s), "version", new_callable=lambda: property(lambda self: "1.0.0")),
|
|
patch("app.config.os.path.dirname"),
|
|
patch("app.config.os.path.join", return_value=str(release_file)),
|
|
patch("app.config.os.path.exists", return_value=True),
|
|
):
|
|
result = s.release_name
|
|
assert result == "Summit"
|
|
|
|
def test_release_name_returns_none_when_no_match(self, tmp_path):
|
|
"""Test release_name returns None when version has no codename."""
|
|
release_data = {
|
|
"releases": {
|
|
"0.5": {"codename": "Foundation"},
|
|
}
|
|
}
|
|
release_file = tmp_path / "release_names.json"
|
|
release_file.write_text(json.dumps(release_data))
|
|
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
with (
|
|
patch.object(type(s), "version", new_callable=lambda: property(lambda self: "0.9.1")),
|
|
patch("app.config.os.path.dirname"),
|
|
patch("app.config.os.path.join", return_value=str(release_file)),
|
|
patch("app.config.os.path.exists", return_value=True),
|
|
):
|
|
result = s.release_name
|
|
assert result is None
|
|
|
|
def test_release_name_returns_none_when_file_missing(self):
|
|
"""Test release_name returns None when release_names.json doesn't exist."""
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
with (
|
|
patch.object(type(s), "version", new_callable=lambda: property(lambda self: "0.5.0")),
|
|
patch("app.config.os.path.dirname"),
|
|
patch("app.config.os.path.join", return_value="/nonexistent/release_names.json"),
|
|
patch("app.config.os.path.exists", return_value=False),
|
|
):
|
|
result = s.release_name
|
|
assert result is None
|
|
|
|
def test_release_name_returns_none_for_unknown_version(self):
|
|
"""Test release_name returns None when version is 'unknown'."""
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
with patch.object(type(s), "version", new_callable=lambda: property(lambda self: "unknown")):
|
|
result = s.release_name
|
|
assert result is None
|
|
|
|
def test_release_name_returns_none_for_empty_version(self):
|
|
"""Test release_name returns None when version is empty."""
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
with patch.object(type(s), "version", new_callable=lambda: property(lambda self: "")):
|
|
result = s.release_name
|
|
assert result is None
|
|
|
|
def test_release_name_handles_invalid_json(self, tmp_path):
|
|
"""Test release_name handles corrupt JSON gracefully."""
|
|
release_file = tmp_path / "release_names.json"
|
|
release_file.write_text("{invalid json")
|
|
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
with (
|
|
patch.object(type(s), "version", new_callable=lambda: property(lambda self: "0.5.0")),
|
|
patch("app.config.os.path.dirname"),
|
|
patch("app.config.os.path.join", return_value=str(release_file)),
|
|
patch("app.config.os.path.exists", return_value=True),
|
|
):
|
|
result = s.release_name
|
|
assert result is None
|
|
|
|
def test_release_name_handles_missing_releases_key(self, tmp_path):
|
|
"""Test release_name handles JSON without 'releases' key."""
|
|
release_file = tmp_path / "release_names.json"
|
|
release_file.write_text(json.dumps({"something_else": {}}))
|
|
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
with (
|
|
patch.object(type(s), "version", new_callable=lambda: property(lambda self: "0.5.0")),
|
|
patch("app.config.os.path.dirname"),
|
|
patch("app.config.os.path.join", return_value=str(release_file)),
|
|
patch("app.config.os.path.exists", return_value=True),
|
|
):
|
|
result = s.release_name
|
|
assert result is None
|
|
|
|
def test_release_name_prefers_exact_over_minor(self, tmp_path):
|
|
"""Test release_name prefers exact version match over minor prefix."""
|
|
release_data = {
|
|
"releases": {
|
|
"0.5.0": {"codename": "ExactMatch"},
|
|
"0.5": {"codename": "MinorMatch"},
|
|
}
|
|
}
|
|
release_file = tmp_path / "release_names.json"
|
|
release_file.write_text(json.dumps(release_data))
|
|
|
|
from app.config import Settings
|
|
|
|
s = Settings.__new__(Settings)
|
|
with (
|
|
patch.object(type(s), "version", new_callable=lambda: property(lambda self: "0.5.0")),
|
|
patch("app.config.os.path.dirname"),
|
|
patch("app.config.os.path.join", return_value=str(release_file)),
|
|
patch("app.config.os.path.exists", return_value=True),
|
|
):
|
|
result = s.release_name
|
|
assert result == "ExactMatch"
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestReleaseNameInStatusView:
|
|
"""Tests for release name display in status dashboard."""
|
|
|
|
@patch("app.views.status.get_provider_status")
|
|
@patch("app.views.status.templates")
|
|
@patch("app.views.status.settings")
|
|
@patch("app.views.status.os.path.exists")
|
|
@pytest.mark.asyncio
|
|
async def test_status_dashboard_includes_release_name(
|
|
self, mock_exists, mock_settings, mock_templates, mock_providers
|
|
):
|
|
"""Test status dashboard passes release_name to template context."""
|
|
from app.views.status import status_dashboard
|
|
|
|
mock_exists.return_value = False
|
|
mock_providers.return_value = {}
|
|
mock_settings.version = "0.5.3"
|
|
mock_settings.build_date = "2024-01-01"
|
|
mock_settings.debug = False
|
|
mock_settings.git_sha = "abc123"
|
|
mock_settings.notification_urls = []
|
|
mock_settings.release_name = "Foundation"
|
|
|
|
mock_request = Mock()
|
|
await status_dashboard(mock_request)
|
|
|
|
call_args = mock_templates.TemplateResponse.call_args
|
|
context = call_args[0][1]
|
|
assert context["release_name"] == "Foundation"
|
|
|
|
@patch("app.views.status.get_provider_status")
|
|
@patch("app.views.status.templates")
|
|
@patch("app.views.status.settings")
|
|
@patch("app.views.status.os.path.exists")
|
|
@pytest.mark.asyncio
|
|
async def test_status_dashboard_handles_missing_release_name(
|
|
self, mock_exists, mock_settings, mock_templates, mock_providers
|
|
):
|
|
"""Test status dashboard handles missing release_name attribute gracefully."""
|
|
from app.views.status import status_dashboard
|
|
|
|
mock_exists.return_value = False
|
|
mock_providers.return_value = {}
|
|
mock_settings.version = "0.9.0"
|
|
mock_settings.build_date = "2024-01-01"
|
|
mock_settings.debug = False
|
|
mock_settings.git_sha = "abc123"
|
|
mock_settings.notification_urls = []
|
|
# Simulate settings without release_name attribute
|
|
del mock_settings.release_name
|
|
|
|
mock_request = Mock()
|
|
await status_dashboard(mock_request)
|
|
|
|
call_args = mock_templates.TemplateResponse.call_args
|
|
context = call_args[0][1]
|
|
assert context["release_name"] is None
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestReleaseNamesJson:
|
|
"""Tests for the release_names.json data file."""
|
|
|
|
def test_release_names_json_is_valid(self):
|
|
"""Test that release_names.json is valid JSON."""
|
|
release_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "release_names.json")
|
|
with open(release_file, "r") as f:
|
|
data = json.load(f)
|
|
|
|
assert "releases" in data
|
|
assert isinstance(data["releases"], dict)
|
|
|
|
def test_release_names_json_entries_have_codename(self):
|
|
"""Test that all entries in release_names.json have a codename."""
|
|
release_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "release_names.json")
|
|
with open(release_file, "r") as f:
|
|
data = json.load(f)
|
|
|
|
for version, entry in data["releases"].items():
|
|
assert "codename" in entry, f"Missing codename for version {version}"
|
|
assert isinstance(entry["codename"], str), f"Codename for {version} must be a string"
|
|
assert len(entry["codename"]) > 0, f"Codename for {version} must not be empty"
|
|
|
|
def test_release_names_json_codenames_are_unique(self):
|
|
"""Test that all codenames in release_names.json are unique."""
|
|
release_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "release_names.json")
|
|
with open(release_file, "r") as f:
|
|
data = json.load(f)
|
|
|
|
codenames = [entry["codename"] for entry in data["releases"].values()]
|
|
assert len(codenames) == len(set(codenames)), "Codenames must be unique"
|