IssueCreator Implementation: - Add tddai/issue_creator.py with full POST API functionality for issue creation - Support multiple creation methods: basic, enhancement, bug, template-based - Include structured issue formatting with acceptance criteria and dependencies - Template system with variable substitution for reusable issue creation Authentication Fix: - Fix critical authentication bug: use GITEA_API_TOKEN instead of GITEA_TOKEN - Update both IssueCreator and IssueWriter for consistency - Update all tests and documentation to reflect correct environment variable Comprehensive Test Suite: - Add 15 unit tests for IssueCreator (tests/test_issue_creator.py) - Add 5 integration tests for full API lifecycle (tests/test_issue_integration.py) - Create test_environment_variable_detection to prevent future auth issues - Total 33 tests covering complete issue handling workflow CLI Integration: - Enhance tddai_cli.py with 3 new commands: create-issue, create-enhancement, create-from-template - Add comprehensive argument parsing with optional fields and priority support - Include user-friendly output with next step guidance - Update package exports to include IssueCreator CLI Roadmap Execution: - Successfully create 8 CLI implementation issues (#12-#19) in Gitea - Resolve mismatch between NEXT.md roadmap and actual Gitea issues - Issues prioritized for core USPs: Database Query CLI and AST Query CLI - Remove local MISSING_ISSUES.md file after successful creation Framework Maturity: - Complete CRUD operations for issue management (Create, Read, Update, Delete) - Robust error handling and API integration patterns - Full authentication and environment variable management - Ready for production CLI implementation workflow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
203 lines
7.8 KiB
Python
203 lines
7.8 KiB
Python
"""
|
|
Tests for IssueWriter functionality.
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from tddai.issue_writer import IssueWriter
|
|
from tddai.exceptions import IssueError
|
|
from tddai.config import TddaiConfig
|
|
from pathlib import Path
|
|
|
|
|
|
class TestIssueWriter:
|
|
"""Test suite for IssueWriter class."""
|
|
|
|
def _get_test_config(self):
|
|
"""Get a valid test configuration."""
|
|
return TddaiConfig(
|
|
workspace_dir=Path(".test_workspace"),
|
|
gitea_url="http://localhost:3000",
|
|
repo_owner="test_owner",
|
|
repo_name="test_repo"
|
|
)
|
|
|
|
def test_init_with_auth_token(self):
|
|
"""Test IssueWriter initialization with auth token."""
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
assert writer.auth_token == "test-token"
|
|
|
|
def test_init_without_auth_token_uses_env(self):
|
|
"""Test IssueWriter uses environment variable when no token provided."""
|
|
config = self._get_test_config()
|
|
with patch.dict('os.environ', {'GITEA_API_TOKEN': 'env-token'}):
|
|
writer = IssueWriter(config=config)
|
|
assert writer.auth_token == "env-token"
|
|
|
|
def test_update_issue_without_auth_token_raises_error(self):
|
|
"""Test that updating without auth token raises IssueError."""
|
|
config = self._get_test_config()
|
|
with patch.dict('os.environ', {}, clear=True):
|
|
writer = IssueWriter(config=config, auth_token=None)
|
|
with pytest.raises(IssueError, match="Authentication token required"):
|
|
writer.update_issue(1, {'title': 'New Title'})
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_update_issue_success(self, mock_run):
|
|
"""Test successful issue update via PATCH."""
|
|
# Mock successful response
|
|
mock_result = MagicMock()
|
|
mock_result.returncode = 0
|
|
mock_result.stdout = json.dumps({
|
|
'number': 1,
|
|
'title': 'Updated Title',
|
|
'body': 'Updated body',
|
|
'state': 'open'
|
|
})
|
|
mock_result.stderr = ''
|
|
mock_run.return_value = mock_result
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
result = writer.update_issue(1, {'title': 'Updated Title'})
|
|
|
|
assert result['number'] == 1
|
|
assert result['title'] == 'Updated Title'
|
|
|
|
# Verify curl command was called correctly
|
|
mock_run.assert_called_once()
|
|
call_args = mock_run.call_args[0][0]
|
|
assert 'curl' in call_args
|
|
assert '-X' in call_args
|
|
assert 'PATCH' in call_args
|
|
assert 'Authorization: token test-token' in ' '.join(call_args)
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_update_issue_with_error_response(self, mock_run):
|
|
"""Test issue update with API error response."""
|
|
mock_result = MagicMock()
|
|
mock_result.returncode = 0
|
|
mock_result.stdout = json.dumps({'message': 'Issue not found'})
|
|
mock_result.stderr = ''
|
|
mock_run.return_value = mock_result
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
with pytest.raises(IssueError, match="Failed to update issue #1: Issue not found"):
|
|
writer.update_issue(1, {'title': 'New Title'})
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_update_issue_subprocess_error(self, mock_run):
|
|
"""Test issue update with subprocess error."""
|
|
mock_run.side_effect = subprocess.CalledProcessError(1, 'curl')
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
with pytest.raises(IssueError, match="Failed to update issue #1"):
|
|
writer.update_issue(1, {'title': 'New Title'})
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_update_issue_json_decode_error(self, mock_run):
|
|
"""Test issue update with invalid JSON response."""
|
|
mock_result = MagicMock()
|
|
mock_result.returncode = 0
|
|
mock_result.stdout = "invalid json"
|
|
mock_result.stderr = ''
|
|
mock_run.return_value = mock_result
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
with pytest.raises(IssueError, match="Failed to parse response data"):
|
|
writer.update_issue(1, {'title': 'New Title'})
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_update_issue_title(self, mock_run):
|
|
"""Test updating only issue title."""
|
|
mock_result = MagicMock()
|
|
mock_result.returncode = 0
|
|
mock_result.stdout = json.dumps({'number': 1, 'title': 'New Title'})
|
|
mock_result.stderr = ''
|
|
mock_run.return_value = mock_result
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
result = writer.update_issue_title(1, 'New Title')
|
|
|
|
assert result['title'] == 'New Title'
|
|
|
|
# Verify the correct data was sent
|
|
call_args = mock_run.call_args[0][0]
|
|
json_data_index = call_args.index('-d') + 1
|
|
sent_data = json.loads(call_args[json_data_index])
|
|
assert sent_data == {'title': 'New Title'}
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_update_issue_body(self, mock_run):
|
|
"""Test updating only issue body."""
|
|
mock_result = MagicMock()
|
|
mock_result.returncode = 0
|
|
mock_result.stdout = json.dumps({'number': 1, 'body': 'New body content'})
|
|
mock_result.stderr = ''
|
|
mock_run.return_value = mock_result
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
result = writer.update_issue_body(1, 'New body content')
|
|
|
|
assert result['body'] == 'New body content'
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_update_issue_state_valid(self, mock_run):
|
|
"""Test updating issue state with valid state."""
|
|
mock_result = MagicMock()
|
|
mock_result.returncode = 0
|
|
mock_result.stdout = json.dumps({'number': 1, 'state': 'closed'})
|
|
mock_result.stderr = ''
|
|
mock_run.return_value = mock_result
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
result = writer.update_issue_state(1, 'closed')
|
|
|
|
assert result['state'] == 'closed'
|
|
|
|
def test_update_issue_state_invalid(self):
|
|
"""Test updating issue state with invalid state."""
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
with pytest.raises(IssueError, match="Invalid state 'invalid'"):
|
|
writer.update_issue_state(1, 'invalid')
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_close_issue(self, mock_run):
|
|
"""Test closing an issue."""
|
|
mock_result = MagicMock()
|
|
mock_result.returncode = 0
|
|
mock_result.stdout = json.dumps({'number': 1, 'state': 'closed'})
|
|
mock_result.stderr = ''
|
|
mock_run.return_value = mock_result
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
result = writer.close_issue(1)
|
|
|
|
assert result['state'] == 'closed'
|
|
|
|
@patch('tddai.issue_writer.subprocess.run')
|
|
def test_reopen_issue(self, mock_run):
|
|
"""Test reopening an issue."""
|
|
mock_result = MagicMock()
|
|
mock_result.returncode = 0
|
|
mock_result.stdout = json.dumps({'number': 1, 'state': 'open'})
|
|
mock_result.stderr = ''
|
|
mock_run.return_value = mock_result
|
|
|
|
config = self._get_test_config()
|
|
writer = IssueWriter(config=config, auth_token="test-token")
|
|
result = writer.reopen_issue(1)
|
|
|
|
assert result['state'] == 'open' |