PyGithub MCP Server

"""Tests for issue-related schema models. This module tests the schema models used for GitHub issue operations. """ import pytest from datetime import datetime from pydantic import ValidationError from pygithub_mcp_server.schemas.issues import ( CreateIssueParams, ListIssuesParams, GetIssueParams, UpdateIssueParams, IssueCommentParams, ListIssueCommentsParams, UpdateIssueCommentParams, DeleteIssueCommentParams, AddIssueLabelsParams, RemoveIssueLabelParams, ) class TestCreateIssueParams: """Tests for the CreateIssueParams schema.""" def test_valid_data(self, valid_create_issue_data): """Test that valid data passes validation.""" params = CreateIssueParams(**valid_create_issue_data) assert params.owner == valid_create_issue_data["owner"] assert params.repo == valid_create_issue_data["repo"] assert params.title == valid_create_issue_data["title"] assert params.body == valid_create_issue_data["body"] assert params.assignees == valid_create_issue_data["assignees"] assert params.labels == valid_create_issue_data["labels"] assert params.milestone == valid_create_issue_data["milestone"] def test_minimal_valid_data(self, valid_repository_ref_data): """Test with minimal valid data (only required fields).""" # Only owner, repo, and title are required params = CreateIssueParams( **valid_repository_ref_data, title="Found a bug" ) assert params.owner == valid_repository_ref_data["owner"] assert params.repo == valid_repository_ref_data["repo"] assert params.title == "Found a bug" assert params.body is None assert params.assignees == [] assert params.labels == [] assert params.milestone is None def test_missing_required_fields(self, valid_repository_ref_data): """Test that missing required fields raise validation errors.""" # Missing title with pytest.raises(ValidationError) as exc_info: CreateIssueParams(**valid_repository_ref_data) assert "title" in str(exc_info.value).lower() def test_invalid_field_types(self, valid_repository_ref_data): """Test that invalid field types raise validation errors.""" # Invalid title type with pytest.raises(ValidationError) as exc_info: CreateIssueParams( **valid_repository_ref_data, title=123 ) assert "title" in str(exc_info.value).lower() # Invalid body type with pytest.raises(ValidationError) as exc_info: CreateIssueParams( **valid_repository_ref_data, title="Found a bug", body=123 ) assert "body" in str(exc_info.value).lower() # Invalid assignees type with pytest.raises(ValidationError) as exc_info: CreateIssueParams( **valid_repository_ref_data, title="Found a bug", assignees="octocat" # Should be a list ) assert "assignees" in str(exc_info.value).lower() # Invalid labels type with pytest.raises(ValidationError) as exc_info: CreateIssueParams( **valid_repository_ref_data, title="Found a bug", labels="bug" # Should be a list ) assert "labels" in str(exc_info.value).lower() # Invalid milestone type with pytest.raises(ValidationError) as exc_info: CreateIssueParams( **valid_repository_ref_data, title="Found a bug", milestone="1" # Should be an integer ) assert "milestone" in str(exc_info.value).lower() def test_empty_strings(self, valid_repository_ref_data): """Test behavior with empty strings.""" # Empty title - should raise error with pytest.raises(ValidationError) as exc_info: CreateIssueParams( **valid_repository_ref_data, title="" ) assert "title cannot be empty" in str(exc_info.value).lower() # Whitespace-only title - should raise error with pytest.raises(ValidationError) as exc_info: CreateIssueParams( **valid_repository_ref_data, title=" " ) assert "title cannot be empty" in str(exc_info.value).lower() # Empty body - should be valid params = CreateIssueParams( **valid_repository_ref_data, title="Found a bug", body="" ) assert params.body == "" def test_none_values(self, valid_repository_ref_data): """Test behavior with None values.""" # None title - should raise error with pytest.raises(ValidationError) as exc_info: CreateIssueParams( **valid_repository_ref_data, title=None ) assert "title" in str(exc_info.value).lower() # None body - should be valid params = CreateIssueParams( **valid_repository_ref_data, title="Found a bug", body=None ) assert params.body is None # None milestone - should be valid params = CreateIssueParams( **valid_repository_ref_data, title="Found a bug", milestone=None ) assert params.milestone is None def test_empty_lists(self, valid_repository_ref_data): """Test behavior with empty lists.""" # Empty assignees - should be valid params = CreateIssueParams( **valid_repository_ref_data, title="Found a bug", assignees=[] ) assert params.assignees == [] # Empty labels - should be valid params = CreateIssueParams( **valid_repository_ref_data, title="Found a bug", labels=[] ) assert params.labels == [] def test_default_values(self, valid_repository_ref_data): """Test that default values are correctly applied.""" params = CreateIssueParams( **valid_repository_ref_data, title="Found a bug" ) assert params.assignees == [] # Default is empty list assert params.labels == [] # Default is empty list class TestListIssuesParams: """Tests for the ListIssuesParams schema.""" def test_valid_data(self, valid_list_issues_data): """Test that valid data passes validation.""" params = ListIssuesParams(**valid_list_issues_data) assert params.owner == valid_list_issues_data["owner"] assert params.repo == valid_list_issues_data["repo"] assert params.state == valid_list_issues_data["state"] assert params.labels == valid_list_issues_data["labels"] assert params.sort == valid_list_issues_data["sort"] assert params.direction == valid_list_issues_data["direction"] # Check that since is a datetime object with the correct values assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 0 assert params.since.minute == 0 assert params.since.second == 0 assert params.page == valid_list_issues_data["page"] assert params.per_page == valid_list_issues_data["per_page"] def test_minimal_valid_data(self, valid_repository_ref_data): """Test with minimal valid data (only required fields).""" # Only owner and repo are required params = ListIssuesParams(**valid_repository_ref_data) assert params.owner == valid_repository_ref_data["owner"] assert params.repo == valid_repository_ref_data["repo"] assert params.state is None assert params.labels is None assert params.sort is None assert params.direction is None assert params.since is None assert params.page is None assert params.per_page is None def test_valid_state_values(self, valid_repository_ref_data): """Test that valid state values pass validation.""" # Valid state values: open, closed, all params = ListIssuesParams(**valid_repository_ref_data, state="open") assert params.state == "open" params = ListIssuesParams(**valid_repository_ref_data, state="closed") assert params.state == "closed" params = ListIssuesParams(**valid_repository_ref_data, state="all") assert params.state == "all" def test_invalid_state_values(self, valid_repository_ref_data): """Test that invalid state values raise validation errors.""" with pytest.raises(ValidationError) as exc_info: ListIssuesParams(**valid_repository_ref_data, state="invalid") assert "Invalid state" in str(exc_info.value) def test_valid_sort_values(self, valid_repository_ref_data): """Test that valid sort values pass validation.""" # Valid sort values: created, updated, comments params = ListIssuesParams(**valid_repository_ref_data, sort="created") assert params.sort == "created" params = ListIssuesParams(**valid_repository_ref_data, sort="updated") assert params.sort == "updated" params = ListIssuesParams(**valid_repository_ref_data, sort="comments") assert params.sort == "comments" def test_invalid_sort_values(self, valid_repository_ref_data): """Test that invalid sort values raise validation errors.""" with pytest.raises(ValidationError) as exc_info: ListIssuesParams(**valid_repository_ref_data, sort="invalid") assert "Invalid sort value" in str(exc_info.value) def test_valid_direction_values(self, valid_repository_ref_data): """Test that valid direction values pass validation.""" # Valid direction values: asc, desc params = ListIssuesParams(**valid_repository_ref_data, direction="asc") assert params.direction == "asc" params = ListIssuesParams(**valid_repository_ref_data, direction="desc") assert params.direction == "desc" def test_invalid_direction_values(self, valid_repository_ref_data): """Test that invalid direction values raise validation errors.""" with pytest.raises(ValidationError) as exc_info: ListIssuesParams(**valid_repository_ref_data, direction="invalid") assert "Invalid direction" in str(exc_info.value) def test_invalid_page_values(self, valid_repository_ref_data): """Test that invalid page values raise validation errors.""" with pytest.raises(ValidationError) as exc_info: ListIssuesParams(**valid_repository_ref_data, page=0) assert "Page number must be a positive integer" in str(exc_info.value) with pytest.raises(ValidationError) as exc_info: ListIssuesParams(**valid_repository_ref_data, page=-1) assert "Page number must be a positive integer" in str(exc_info.value) def test_invalid_per_page_values(self, valid_repository_ref_data): """Test that invalid per_page values raise validation errors.""" with pytest.raises(ValidationError) as exc_info: ListIssuesParams(**valid_repository_ref_data, per_page=0) assert "Results per page must be a positive integer" in str(exc_info.value) with pytest.raises(ValidationError) as exc_info: ListIssuesParams(**valid_repository_ref_data, per_page=-1) assert "Results per page must be a positive integer" in str(exc_info.value) with pytest.raises(ValidationError) as exc_info: ListIssuesParams(**valid_repository_ref_data, per_page=101) assert "Results per page cannot exceed 100" in str(exc_info.value) def test_none_per_page_value(self, valid_repository_ref_data): """Test that None per_page value is valid.""" params = ListIssuesParams(**valid_repository_ref_data, per_page=None) assert params.per_page is None def test_datetime_parsing(self, valid_repository_ref_data): """Test that datetime strings are correctly parsed.""" # ISO format datetime string params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T00:00:00Z" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 0 assert params.since.minute == 0 assert params.since.second == 0 # Test with different ISO format (positive timezone offset) params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45+00:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with datetime object directly dt = datetime(2020, 1, 1, 0, 0, 0, tzinfo=datetime.now().astimezone().tzinfo) params = ListIssuesParams( **valid_repository_ref_data, since=dt ) assert params.since == dt def test_timezone_formats(self, valid_repository_ref_data): """Test various timezone formats in datetime strings.""" # Test with standard negative timezone offset params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45-05:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with timezone format that has a colon (no normalization needed) params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45+05:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with timezone format that doesn't have 5 chars (e.g., +05) params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45+05" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with negative timezone offset without colon params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45-0500" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with timezone format that has no sign (Z) params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45Z" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with single-digit negative timezone offset params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45-01:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with extreme negative timezone offset params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45-12:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with positive timezone offset without colon params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45+0500" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with timezone format that doesn't need normalization params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45+05:30" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 def test_invalid_datetime_format(self, valid_repository_ref_data): """Test behavior with invalid datetime format.""" # Missing time component with pytest.raises(ValidationError) as exc_info: ListIssuesParams( **valid_repository_ref_data, since="2020-01-01" # Missing time component ) assert "Invalid ISO format datetime" in str(exc_info.value) # Missing timezone with pytest.raises(ValidationError) as exc_info: ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T00:00:00" # No timezone ) assert "Invalid ISO format datetime" in str(exc_info.value) # Space instead of 'T' with pytest.raises(ValidationError) as exc_info: ListIssuesParams( **valid_repository_ref_data, since="2020-01-01 00:00:00Z" # Space instead of 'T' ) assert "Invalid ISO format datetime" in str(exc_info.value) # Completely invalid format with pytest.raises(ValidationError) as exc_info: ListIssuesParams( **valid_repository_ref_data, since="not-a-date" ) assert "Invalid ISO format datetime" in str(exc_info.value) # Malformed but plausible ISO format (passes regex check but fails parsing) with pytest.raises(ValidationError) as exc_info: ListIssuesParams( **valid_repository_ref_data, since="2020-13-32T25:61:61Z" # Invalid month, day, hour, minute, second ) assert "Invalid ISO format datetime" in str(exc_info.value) # Test with single-digit timezone format (now supported) params = ListIssuesParams( **valid_repository_ref_data, since="2020-01-01T12:30:45-5" # Single-digit timezone format ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 class TestGetIssueParams: """Tests for the GetIssueParams schema.""" def test_valid_data(self, valid_get_issue_data): """Test that valid data passes validation.""" params = GetIssueParams(**valid_get_issue_data) assert params.owner == valid_get_issue_data["owner"] assert params.repo == valid_get_issue_data["repo"] assert params.issue_number == valid_get_issue_data["issue_number"] def test_missing_required_fields(self, valid_repository_ref_data): """Test that missing required fields raise validation errors.""" # Missing issue_number with pytest.raises(ValidationError) as exc_info: GetIssueParams(**valid_repository_ref_data) assert "issue_number" in str(exc_info.value).lower() def test_invalid_issue_number_type(self, valid_repository_ref_data): """Test that invalid issue_number type raises validation error.""" # String instead of integer with pytest.raises(ValidationError) as exc_info: GetIssueParams( **valid_repository_ref_data, issue_number="1" # Should be an integer ) assert "issue_number" in str(exc_info.value).lower() def test_negative_issue_number(self, valid_repository_ref_data): """Test behavior with negative issue number.""" # Negative issue number - should be valid (though not practical) params = GetIssueParams( **valid_repository_ref_data, issue_number=-1 ) assert params.issue_number == -1 class TestUpdateIssueParams: """Tests for the UpdateIssueParams schema.""" def test_valid_data(self, valid_update_issue_data): """Test that valid data passes validation.""" params = UpdateIssueParams(**valid_update_issue_data) assert params.owner == valid_update_issue_data["owner"] assert params.repo == valid_update_issue_data["repo"] assert params.issue_number == valid_update_issue_data["issue_number"] assert params.title == valid_update_issue_data["title"] assert params.body == valid_update_issue_data["body"] assert params.state == valid_update_issue_data["state"] assert params.labels == valid_update_issue_data["labels"] assert params.assignees == valid_update_issue_data["assignees"] assert params.milestone == valid_update_issue_data["milestone"] def test_minimal_valid_data(self, valid_repository_ref_data): """Test with minimal valid data (only required fields).""" # Only owner, repo, and issue_number are required params = UpdateIssueParams( **valid_repository_ref_data, issue_number=1 ) assert params.owner == valid_repository_ref_data["owner"] assert params.repo == valid_repository_ref_data["repo"] assert params.issue_number == 1 assert params.title is None assert params.body is None assert params.state is None assert params.labels is None assert params.assignees is None assert params.milestone is None def test_partial_update(self, valid_repository_ref_data): """Test updating only some fields.""" # Update only title and state params = UpdateIssueParams( **valid_repository_ref_data, issue_number=1, title="Updated bug report", state="closed" ) assert params.title == "Updated bug report" assert params.state == "closed" assert params.body is None assert params.labels is None assert params.assignees is None assert params.milestone is None def test_valid_state_values(self, valid_repository_ref_data): """Test that valid state values pass validation.""" # Valid state values: open, closed params = UpdateIssueParams( **valid_repository_ref_data, issue_number=1, state="open" ) assert params.state == "open" params = UpdateIssueParams( **valid_repository_ref_data, issue_number=1, state="closed" ) assert params.state == "closed" def test_invalid_state_values(self, valid_repository_ref_data): """Test that invalid state values raise validation errors.""" # 'all' is not valid for UpdateIssueParams with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, state="all" ) assert "Invalid state" in str(exc_info.value) # Other invalid values with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, state="invalid" ) assert "Invalid state" in str(exc_info.value) def test_empty_title(self, valid_repository_ref_data): """Test that empty title raises validation error.""" with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, title="" ) assert "title cannot be empty" in str(exc_info.value) with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, title=" " # Whitespace only ) assert "title cannot be empty" in str(exc_info.value) def test_none_values(self, valid_repository_ref_data): """Test behavior with None values.""" # All optional fields can be None params = UpdateIssueParams( **valid_repository_ref_data, issue_number=1, title=None, body=None, state=None, labels=None, assignees=None, milestone=None ) assert params.title is None assert params.body is None assert params.state is None assert params.labels is None assert params.assignees is None assert params.milestone is None def test_title_validation_edge_cases(self, valid_repository_ref_data): """Test edge cases for title validation.""" # Test with None title (should be valid) params = UpdateIssueParams( **valid_repository_ref_data, issue_number=1, title=None ) assert params.title is None def test_invalid_field_types(self, valid_repository_ref_data): """Test that invalid field types raise validation errors.""" # Invalid issue_number type with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number="1" # Should be an integer ) assert "issue_number" in str(exc_info.value).lower() # Invalid title type with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, title=123 # Should be a string ) assert "title" in str(exc_info.value).lower() # Invalid body type with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, body=123 # Should be a string ) assert "body" in str(exc_info.value).lower() # Invalid state type with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, state=123 # Should be a string ) assert "state" in str(exc_info.value).lower() # Invalid labels type with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, labels="bug" # Should be a list ) assert "labels" in str(exc_info.value).lower() # Invalid assignees type with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, assignees="octocat" # Should be a list ) assert "assignees" in str(exc_info.value).lower() # Invalid milestone type with pytest.raises(ValidationError) as exc_info: UpdateIssueParams( **valid_repository_ref_data, issue_number=1, milestone="1" # Should be an integer ) assert "milestone" in str(exc_info.value).lower() class TestUpdateIssueCommentParams: """Tests for the UpdateIssueCommentParams schema.""" def test_valid_data(self, valid_update_issue_comment_data): """Test that valid data passes validation.""" params = UpdateIssueCommentParams(**valid_update_issue_comment_data) assert params.owner == valid_update_issue_comment_data["owner"] assert params.repo == valid_update_issue_comment_data["repo"] assert params.issue_number == valid_update_issue_comment_data["issue_number"] assert params.comment_id == valid_update_issue_comment_data["comment_id"] assert params.body == valid_update_issue_comment_data["body"] def test_missing_required_fields(self, valid_repository_ref_data): """Test that missing required fields raise validation errors.""" # Missing issue_number with pytest.raises(ValidationError) as exc_info: UpdateIssueCommentParams( **valid_repository_ref_data, comment_id=123456, body="Updated comment text." ) assert "issue_number" in str(exc_info.value).lower() # Missing comment_id with pytest.raises(ValidationError) as exc_info: UpdateIssueCommentParams( **valid_repository_ref_data, issue_number=1, body="Updated comment text." ) assert "comment_id" in str(exc_info.value).lower() # Missing body with pytest.raises(ValidationError) as exc_info: UpdateIssueCommentParams( **valid_repository_ref_data, issue_number=1, comment_id=123456 ) assert "body" in str(exc_info.value).lower() def test_empty_body(self, valid_repository_ref_data): """Test that empty body raises validation error.""" # Empty body with pytest.raises(ValidationError) as exc_info: UpdateIssueCommentParams( **valid_repository_ref_data, issue_number=1, comment_id=123456, body="" ) assert "body cannot be empty" in str(exc_info.value).lower() # Whitespace-only body with pytest.raises(ValidationError) as exc_info: UpdateIssueCommentParams( **valid_repository_ref_data, issue_number=1, comment_id=123456, body=" " ) assert "body cannot be empty" in str(exc_info.value).lower() def test_invalid_field_types(self, valid_repository_ref_data): """Test that invalid field types raise validation errors.""" # Invalid issue_number type with pytest.raises(ValidationError) as exc_info: UpdateIssueCommentParams( **valid_repository_ref_data, issue_number="1", # Should be an integer comment_id=123456, body="Updated comment text." ) assert "issue_number" in str(exc_info.value).lower() # Invalid comment_id type with pytest.raises(ValidationError) as exc_info: UpdateIssueCommentParams( **valid_repository_ref_data, issue_number=1, comment_id="123456", # Should be an integer body="Updated comment text." ) assert "comment_id" in str(exc_info.value).lower() # Invalid body type with pytest.raises(ValidationError) as exc_info: UpdateIssueCommentParams( **valid_repository_ref_data, issue_number=1, comment_id=123456, body=123 # Should be a string ) assert "body" in str(exc_info.value).lower() class TestDeleteIssueCommentParams: """Tests for the DeleteIssueCommentParams schema.""" def test_valid_data(self, valid_delete_issue_comment_data): """Test that valid data passes validation.""" params = DeleteIssueCommentParams(**valid_delete_issue_comment_data) assert params.owner == valid_delete_issue_comment_data["owner"] assert params.repo == valid_delete_issue_comment_data["repo"] assert params.issue_number == valid_delete_issue_comment_data["issue_number"] assert params.comment_id == valid_delete_issue_comment_data["comment_id"] def test_missing_required_fields(self, valid_repository_ref_data): """Test that missing required fields raise validation errors.""" # Missing issue_number with pytest.raises(ValidationError) as exc_info: DeleteIssueCommentParams( **valid_repository_ref_data, comment_id=123456 ) assert "issue_number" in str(exc_info.value).lower() # Missing comment_id with pytest.raises(ValidationError) as exc_info: DeleteIssueCommentParams( **valid_repository_ref_data, issue_number=1 ) assert "comment_id" in str(exc_info.value).lower() def test_negative_issue_number(self, valid_repository_ref_data): """Test behavior with negative issue number.""" # Negative issue number - should be valid (though not practical) params = DeleteIssueCommentParams( **valid_repository_ref_data, issue_number=-1, comment_id=123456 ) assert params.issue_number == -1 def test_invalid_field_types(self, valid_repository_ref_data): """Test that invalid field types raise validation errors.""" # Invalid issue_number type with pytest.raises(ValidationError) as exc_info: DeleteIssueCommentParams( **valid_repository_ref_data, issue_number="1", # Should be an integer comment_id=123456 ) assert "issue_number" in str(exc_info.value).lower() # Invalid comment_id type with pytest.raises(ValidationError) as exc_info: DeleteIssueCommentParams( **valid_repository_ref_data, issue_number=1, comment_id="123456" # Should be an integer ) assert "comment_id" in str(exc_info.value).lower() class TestAddIssueLabelsParams: """Tests for the AddIssueLabelsParams schema.""" def test_valid_data(self, valid_add_issue_labels_data): """Test that valid data passes validation.""" params = AddIssueLabelsParams(**valid_add_issue_labels_data) assert params.owner == valid_add_issue_labels_data["owner"] assert params.repo == valid_add_issue_labels_data["repo"] assert params.issue_number == valid_add_issue_labels_data["issue_number"] assert params.labels == valid_add_issue_labels_data["labels"] def test_missing_required_fields(self, valid_repository_ref_data): """Test that missing required fields raise validation errors.""" # Missing issue_number with pytest.raises(ValidationError) as exc_info: AddIssueLabelsParams( **valid_repository_ref_data, labels=["bug", "help wanted"] ) assert "issue_number" in str(exc_info.value).lower() # Missing labels with pytest.raises(ValidationError) as exc_info: AddIssueLabelsParams( **valid_repository_ref_data, issue_number=1 ) assert "labels" in str(exc_info.value).lower() def test_empty_labels_list(self, valid_repository_ref_data): """Test behavior with empty labels list.""" # Empty labels list - should raise validation error with pytest.raises(ValidationError) as exc_info: AddIssueLabelsParams( **valid_repository_ref_data, issue_number=1, labels=[] ) assert "labels list cannot be empty" in str(exc_info.value).lower() def test_negative_issue_number(self, valid_repository_ref_data): """Test behavior with negative issue number.""" # Negative issue number - should be valid (though not practical) params = AddIssueLabelsParams( **valid_repository_ref_data, issue_number=-1, labels=["bug", "help wanted"] ) assert params.issue_number == -1 def test_invalid_field_types(self, valid_repository_ref_data): """Test that invalid field types raise validation errors.""" # Invalid issue_number type with pytest.raises(ValidationError) as exc_info: AddIssueLabelsParams( **valid_repository_ref_data, issue_number="1", # Should be an integer labels=["bug", "help wanted"] ) assert "issue_number" in str(exc_info.value).lower() # Invalid labels type with pytest.raises(ValidationError) as exc_info: AddIssueLabelsParams( **valid_repository_ref_data, issue_number=1, labels="bug" # Should be a list ) assert "labels" in str(exc_info.value).lower() # Invalid label item type with pytest.raises(ValidationError) as exc_info: AddIssueLabelsParams( **valid_repository_ref_data, issue_number=1, labels=["bug", 123] # All items should be strings ) assert "labels" in str(exc_info.value).lower() class TestRemoveIssueLabelParams: """Tests for the RemoveIssueLabelParams schema.""" def test_valid_data(self, valid_remove_issue_label_data): """Test that valid data passes validation.""" params = RemoveIssueLabelParams(**valid_remove_issue_label_data) assert params.owner == valid_remove_issue_label_data["owner"] assert params.repo == valid_remove_issue_label_data["repo"] assert params.issue_number == valid_remove_issue_label_data["issue_number"] assert params.label == valid_remove_issue_label_data["label"] def test_missing_required_fields(self, valid_repository_ref_data): """Test that missing required fields raise validation errors.""" # Missing issue_number with pytest.raises(ValidationError) as exc_info: RemoveIssueLabelParams( **valid_repository_ref_data, label="help wanted" ) assert "issue_number" in str(exc_info.value).lower() # Missing label with pytest.raises(ValidationError) as exc_info: RemoveIssueLabelParams( **valid_repository_ref_data, issue_number=1 ) assert "label" in str(exc_info.value).lower() def test_empty_label(self, valid_repository_ref_data): """Test behavior with empty label.""" # Empty label - should raise error with pytest.raises(ValidationError) as exc_info: RemoveIssueLabelParams( **valid_repository_ref_data, issue_number=1, label="" ) assert "label cannot be empty" in str(exc_info.value).lower() # Whitespace-only label - should raise error with pytest.raises(ValidationError) as exc_info: RemoveIssueLabelParams( **valid_repository_ref_data, issue_number=1, label=" " ) assert "label cannot be empty" in str(exc_info.value).lower() def test_invalid_field_types(self, valid_repository_ref_data): """Test that invalid field types raise validation errors.""" # Invalid issue_number type with pytest.raises(ValidationError) as exc_info: RemoveIssueLabelParams( **valid_repository_ref_data, issue_number="1", # Should be an integer label="help wanted" ) assert "issue_number" in str(exc_info.value).lower() # Invalid label type with pytest.raises(ValidationError) as exc_info: RemoveIssueLabelParams( **valid_repository_ref_data, issue_number=1, label=123 # Should be a string ) assert "label" in str(exc_info.value).lower() class TestIssueCommentParams: """Tests for the IssueCommentParams schema.""" def test_valid_data(self, valid_issue_comment_data): """Test that valid data passes validation.""" params = IssueCommentParams(**valid_issue_comment_data) assert params.owner == valid_issue_comment_data["owner"] assert params.repo == valid_issue_comment_data["repo"] assert params.issue_number == valid_issue_comment_data["issue_number"] assert params.body == valid_issue_comment_data["body"] def test_missing_required_fields(self, valid_repository_ref_data): """Test that missing required fields raise validation errors.""" # Missing issue_number with pytest.raises(ValidationError) as exc_info: IssueCommentParams( **valid_repository_ref_data, body="This is a comment." ) assert "issue_number" in str(exc_info.value).lower() # Missing body with pytest.raises(ValidationError) as exc_info: IssueCommentParams( **valid_repository_ref_data, issue_number=1 ) assert "body" in str(exc_info.value).lower() def test_empty_body(self, valid_repository_ref_data): """Test behavior with empty body.""" # Empty body - should raise error with pytest.raises(ValidationError) as exc_info: IssueCommentParams( **valid_repository_ref_data, issue_number=1, body="" ) assert "body cannot be empty" in str(exc_info.value).lower() # Whitespace-only body - should raise error with pytest.raises(ValidationError) as exc_info: IssueCommentParams( **valid_repository_ref_data, issue_number=1, body=" " ) assert "body cannot be empty" in str(exc_info.value).lower() def test_invalid_field_types(self, valid_repository_ref_data): """Test that invalid field types raise validation errors.""" # Invalid issue_number type with pytest.raises(ValidationError) as exc_info: IssueCommentParams( **valid_repository_ref_data, issue_number="1", # Should be an integer body="This is a comment." ) assert "issue_number" in str(exc_info.value).lower() # Invalid body type with pytest.raises(ValidationError) as exc_info: IssueCommentParams( **valid_repository_ref_data, issue_number=1, body=123 # Should be a string ) assert "body" in str(exc_info.value).lower() class TestListIssueCommentsParams: """Tests for the ListIssueCommentsParams schema.""" def test_valid_data(self, valid_list_issue_comments_data): """Test that valid data passes validation.""" params = ListIssueCommentsParams(**valid_list_issue_comments_data) assert params.owner == valid_list_issue_comments_data["owner"] assert params.repo == valid_list_issue_comments_data["repo"] assert params.issue_number == valid_list_issue_comments_data["issue_number"] # Check that since is a datetime object with the correct values assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 0 assert params.since.minute == 0 assert params.since.second == 0 assert params.page == valid_list_issue_comments_data["page"] assert params.per_page == valid_list_issue_comments_data["per_page"] def test_minimal_valid_data(self, valid_repository_ref_data): """Test with minimal valid data (only required fields).""" # Only owner, repo, and issue_number are required params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1 ) assert params.owner == valid_repository_ref_data["owner"] assert params.repo == valid_repository_ref_data["repo"] assert params.issue_number == 1 assert params.since is None assert params.page is None assert params.per_page is None def test_datetime_parsing(self, valid_repository_ref_data): """Test that datetime strings are correctly parsed.""" # ISO format datetime string params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T00:00:00Z" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 0 assert params.since.minute == 0 assert params.since.second == 0 # Test with different ISO format (positive timezone offset) params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45+00:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with datetime object directly dt = datetime(2020, 1, 1, 0, 0, 0, tzinfo=datetime.now().astimezone().tzinfo) params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since=dt ) assert params.since == dt def test_timezone_formats(self, valid_repository_ref_data): """Test various timezone formats in datetime strings.""" # Test with standard negative timezone offset params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45-05:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with negative timezone offset without colon params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45-0500" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with timezone format that has no sign (Z) params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45Z" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with single-digit negative timezone offset params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45-01:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with extreme negative timezone offset params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45-12:00" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with positive timezone offset without colon params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45+0500" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 # Test with timezone format that doesn't need normalization params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45+05:30" ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 def test_invalid_datetime_format(self, valid_repository_ref_data): """Test behavior with invalid datetime format.""" # Missing time component with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01" # Missing time component ) assert "Invalid ISO format datetime" in str(exc_info.value) # Missing timezone with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T00:00:00" # No timezone ) assert "Invalid ISO format datetime" in str(exc_info.value) # Space instead of 'T' with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01 00:00:00Z" # Space instead of 'T' ) assert "Invalid ISO format datetime" in str(exc_info.value) # Completely invalid format with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="not-a-date" ) assert "Invalid ISO format datetime" in str(exc_info.value) # Malformed but plausible ISO format (passes regex check but fails parsing) with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-13-32T25:61:61Z" # Invalid month, day, hour, minute, second ) assert "Invalid ISO format datetime" in str(exc_info.value) # Test with single-digit timezone format (now supported) params = ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since="2020-01-01T12:30:45-5" # Single-digit timezone format ) assert isinstance(params.since, datetime) assert params.since.year == 2020 assert params.since.month == 1 assert params.since.day == 1 assert params.since.hour == 12 assert params.since.minute == 30 assert params.since.second == 45 def test_invalid_field_types(self, valid_repository_ref_data): """Test that invalid field types raise validation errors.""" # Invalid issue_number type with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number="1", # Should be an integer since="2020-01-01T00:00:00Z" ) assert "issue_number" in str(exc_info.value).lower() # Invalid since type (not a string or datetime) with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, since=123 # Should be a string or datetime ) assert "since" in str(exc_info.value).lower() # Invalid page type with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, page="1" # Should be an integer ) assert "page" in str(exc_info.value).lower() # Invalid per_page type with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, per_page="30" # Should be an integer ) assert "per_page" in str(exc_info.value).lower() def test_invalid_page_values(self, valid_repository_ref_data): """Test that invalid page values raise validation errors.""" # Page 0 (invalid) with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, page=0 ) assert "Page number must be a positive integer" in str(exc_info.value) # Negative page (invalid) with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, page=-1 ) assert "Page number must be a positive integer" in str(exc_info.value) def test_invalid_per_page_values(self, valid_repository_ref_data): """Test that invalid per_page values raise validation errors.""" # per_page 0 (invalid) with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, per_page=0 ) assert "Results per page must be a positive integer" in str(exc_info.value) # Negative per_page (invalid) with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, per_page=-1 ) assert "Results per page must be a positive integer" in str(exc_info.value) # per_page > 100 (invalid) with pytest.raises(ValidationError) as exc_info: ListIssueCommentsParams( **valid_repository_ref_data, issue_number=1, per_page=101 ) assert "Results per page cannot exceed 100" in str(exc_info.value)