test: implement unit testing suite with pytest and add pre-push verification hook
CI/CD Pipeline / Lint & Check (push) Successful in 10s
CI/CD Pipeline / Build & Push Docker Image (push) Successful in 1m21s

This commit is contained in:
2026-05-13 00:12:32 +02:00
parent 8e9e4c01d4
commit 99fd37fc12
8 changed files with 392 additions and 20 deletions
+1
View File
@@ -0,0 +1 @@
# This file marks the tests/unit directory as a Python package.
+63
View File
@@ -0,0 +1,63 @@
"""Unit tests for MCP content block helper functions."""
import json
from mcp.types import TextContent, EmbeddedResource
# Import helpers directly from a tool module to test them
from strava_mcp_server.tools.activities import _resource, _user_text
class TestUserTextHelper:
def test_returns_text_content(self):
result = _user_text("Hello World")
assert isinstance(result, TextContent)
def test_type_is_text(self):
result = _user_text("Hello")
assert result.type == "text"
def test_text_value(self):
result = _user_text("Test message")
assert result.text == "Test message"
def test_audience_is_user(self):
result = _user_text("Hello")
assert result.annotations is not None
assert result.annotations.audience == ["user"]
class TestResourceHelper:
def test_returns_embedded_resource(self):
result = _resource("internal://test/data", {"key": "value"})
assert isinstance(result, EmbeddedResource)
def test_type_is_resource(self):
result = _resource("internal://test/data", {"key": "value"})
assert result.type == "resource"
def test_mime_type_is_json(self):
result = _resource("internal://test/data", {"key": "value"})
assert result.resource.mimeType == "application/json"
def test_uri_is_set(self):
result = _resource("internal://athlete/profile", {"id": 1})
assert str(result.resource.uri) == "internal://athlete/profile"
def test_text_is_valid_json(self):
data = {"id": 42, "name": "Test Athlete", "active": True}
result = _resource("internal://test", data)
parsed = json.loads(result.resource.text)
assert parsed == data
def test_audience_is_assistant(self):
result = _resource("internal://test", {})
assert result.annotations is not None
assert result.annotations.audience == ["assistant"]
def test_list_data(self):
data = [{"id": 1}, {"id": 2}]
result = _resource("internal://activities/list", data)
parsed = json.loads(result.resource.text)
assert len(parsed) == 2
assert parsed[0]["id"] == 1
+98
View File
@@ -0,0 +1,98 @@
"""Unit tests for strava_mcp_server.utils — pure functions, no external dependencies."""
from datetime import datetime, timezone
from strava_mcp_server.utils import (
parse_iso_to_unix,
format_date_iso,
format_date_human,
)
class TestParseIsoToUnix:
def test_full_iso_with_z(self):
result = parse_iso_to_unix("2024-01-15T12:00:00Z")
assert result == int(
datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc).timestamp()
)
def test_full_iso_with_offset(self):
result = parse_iso_to_unix("2024-01-15T13:00:00+01:00")
assert result == int(
datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc).timestamp()
)
def test_date_only(self):
result = parse_iso_to_unix("2024-06-01")
assert result == int(
datetime(2024, 6, 1, 0, 0, 0, tzinfo=timezone.utc).timestamp()
)
def test_none_returns_none(self):
assert parse_iso_to_unix(None) is None
def test_empty_string_returns_none(self):
assert parse_iso_to_unix("") is None
def test_invalid_string_returns_none(self):
assert parse_iso_to_unix("not-a-date") is None
def test_returns_int(self):
result = parse_iso_to_unix("2024-01-01T00:00:00Z")
assert isinstance(result, int)
class TestFormatDateIso:
def test_z_suffix_normalized(self):
result = format_date_iso("2024-01-15T12:00:00Z")
assert result == "2024-01-15T12:00:00Z"
def test_offset_preserved(self):
# format_date_iso preserves timezone offset info; normalization to UTC
# is the caller's responsibility if needed.
result = format_date_iso("2024-01-15T13:00:00+01:00")
assert result == "2024-01-15T13:00:00+01:00"
def test_datetime_object(self):
dt = datetime(2024, 3, 10, 8, 30, 0, tzinfo=timezone.utc)
result = format_date_iso(dt)
assert result == "2024-03-10T08:30:00Z"
def test_none_returns_na(self):
assert format_date_iso(None) == "N/A"
def test_empty_string_returns_na(self):
assert format_date_iso("") == "N/A"
def test_invalid_string_returned_as_is(self):
result = format_date_iso("not-a-date")
assert result == "not-a-date"
class TestFormatDateHuman:
def test_iso_with_z(self):
result = format_date_human("2024-01-15T08:30:00Z")
assert result == "15.01.2024 08:30"
def test_iso_with_offset(self):
# +01:00 → displayed in local time of the datetime (13:00 in +01 = 13:00 displayed)
result = format_date_human("2024-01-15T13:45:00+01:00")
assert result == "15.01.2024 13:45"
def test_datetime_object(self):
dt = datetime(2024, 12, 31, 23, 59, tzinfo=timezone.utc)
result = format_date_human(dt)
assert result == "31.12.2024 23:59"
def test_none_returns_na(self):
assert format_date_human(None) == "N/A"
def test_empty_string_returns_na(self):
assert format_date_human("") == "N/A"
def test_format_pattern(self):
result = format_date_human("2024-06-01T00:00:00Z")
# Ensure it matches DD.MM.YYYY HH:MM
import re
assert re.match(r"\d{2}\.\d{2}\.\d{4} \d{2}:\d{2}", result)