Skip to content
Open
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a36883c
Add bi-directional sync feature
Zahed-Riyaz Aug 11, 2025
3c2ca3d
Clean up files
Zahed-Riyaz Aug 16, 2025
45e6370
Rename migration file
Zahed-Riyaz Aug 20, 2025
61b0c24
Add github_token to serializer fields and requests
Zahed-Riyaz Aug 23, 2025
c9db18a
Update sync logic
Zahed-Riyaz Aug 24, 2025
6f59a0d
Implement middleware logic
Zahed-Riyaz Aug 25, 2025
79771c4
Fix field order
Zahed-Riyaz Aug 25, 2025
3d97a03
Omit debug statements
Zahed-Riyaz Aug 25, 2025
9c2b699
Fix github_branch migration file
Zahed-Riyaz Aug 26, 2025
650d823
Pass linting checks
Zahed-Riyaz Aug 26, 2025
cab85e1
Merge branch 'master' into github-sync
Zahed-Riyaz Aug 26, 2025
d581b4b
Add tests
Zahed-Riyaz Aug 26, 2025
10ee702
Add tests for github_utils
Zahed-Riyaz Aug 26, 2025
2eaa465
Pass code quality checks
Zahed-Riyaz Aug 26, 2025
f13a78d
Modify tests
Zahed-Riyaz Aug 26, 2025
dff14ee
Omit non essential model fields
Zahed-Riyaz Aug 26, 2025
8ec3b79
Merge branch 'master' into github-sync
RishabhJain2018 Sep 1, 2025
6686c03
Merge branch 'master' into github-sync
Zahed-Riyaz Sep 1, 2025
d277d88
Pass code quality checks
Zahed-Riyaz Sep 1, 2025
33a7677
Merge branch 'master' into github-sync
Zahed-Riyaz Sep 1, 2025
4a05fa5
Pass code quality checks
Zahed-Riyaz Sep 1, 2025
9cd82fc
Add coverage for missing lines
Zahed-Riyaz Sep 2, 2025
aefbf7a
Merge branch 'master' into github-sync
Zahed-Riyaz Sep 2, 2025
9584291
Fix TestDeserializeObject error
Zahed-Riyaz Sep 2, 2025
65cf97f
Update .travis.yml
Zahed-Riyaz Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add tests for github_utils
  • Loading branch information
Zahed-Riyaz committed Aug 26, 2025
commit 10ee70261d50e0eec3d47437f0084d83c77c309e
198 changes: 80 additions & 118 deletions tests/unit/challenges/test_github_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
import logging
from unittest.mock import Mock, patch

from challenges.github_interface import GithubInterface
from challenges.github_utils import (
GithubInterface,
sync_challenge_phase_to_github,
sync_challenge_to_github,
github_challenge_phase_sync,
github_challenge_sync,
)
from challenges.models import Challenge, ChallengePhase
from django.test import TestCase
from django.utils import timezone
from django.contrib.auth.models import User
from hosts.models import ChallengeHostTeam


class TestGithubInterface(TestCase):
Expand All @@ -19,16 +21,15 @@ def setUp(self):
self.token = "test_token"
self.repo = "test/repo"
self.branch = "master"
self.github = GithubInterface(self.token, self.repo, self.branch)
self.github = GithubInterface(self.repo, self.branch, self.token)

def test_init(self):
"""Test GithubInterface initialization"""
self.assertEqual(self.github.token, self.token)
self.assertEqual(self.github.repo, self.repo)
self.assertEqual(self.github.branch, self.branch)
self.assertEqual(self.github.base_url, "https://api.github.com")
self.assertIn("Authorization", self.github.headers)
self.assertIn("Accept", self.github.headers)
self.assertEqual(self.github.GITHUB_AUTH_TOKEN, self.token)
self.assertEqual(self.github.GITHUB_REPOSITORY, self.repo)
self.assertEqual(self.github.BRANCH, self.branch)
headers = self.github.get_request_headers()
self.assertIn("Authorization", headers)

def test_get_github_url(self):
"""Test get_github_url method"""
Expand All @@ -37,105 +38,93 @@ def test_get_github_url(self):
result = self.github.get_github_url(url)
self.assertEqual(result, expected)

@patch("challenges.github_utils.requests.get")
def test_get_file_contents_success(self, mock_get):
"""Test get_file_contents with successful response"""
@patch("challenges.github_interface.requests.request")
def test_get_content_from_path_success(self, mock_request):
"""Test get_content_from_path with successful response"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"content": "test content"}
mock_get.return_value = mock_response
mock_response.raise_for_status.return_value = None
mock_request.return_value = mock_response

result = self.github.get_file_contents("test.json")
result = self.github.get_content_from_path("test.json")

self.assertEqual(result, {"content": "test content"})
mock_get.assert_called_once()
mock_request.assert_called_once()

@patch("challenges.github_interface.requests.request")
def test_get_content_from_path_not_found(self, mock_request):
"""Test get_content_from_path with error response returns None"""
from requests.exceptions import RequestException

@patch("challenges.github_utils.requests.get")
def test_get_file_contents_not_found(self, mock_get):
"""Test get_file_contents with 404 response"""
mock_response = Mock()
mock_response.status_code = 404
mock_get.return_value = mock_response
mock_response.raise_for_status.side_effect = RequestException()
mock_request.return_value = mock_response

result = self.github.get_file_contents("test.json")
result = self.github.get_content_from_path("test.json")

self.assertIsNone(result)

@patch("challenges.github_utils.requests.put")
def test_update_text_file_success(self, mock_put):
"""Test update_text_file with successful response"""
@patch("challenges.github_interface.GithubInterface.get_content_from_path")
@patch("challenges.github_interface.requests.request")
def test_update_content_from_path_success(self, mock_request, mock_get):
"""Test update_content_from_path with successful response"""
# Simulate existing file with sha so update path is used
mock_get.return_value = {"sha": "old_sha"}
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"sha": "test_sha"}
mock_put.return_value = mock_response
mock_response.json.return_value = {"sha": "new_sha"}
mock_response.raise_for_status.return_value = None
mock_request.return_value = mock_response

result = self.github.update_text_file(
"test.json", "content", "message", "sha"
result = self.github.update_content_from_path(
"test.json", "Y29udGVudA==", changed_field="title"
)

self.assertEqual(result, {"sha": "test_sha"})
mock_put.assert_called_once()
self.assertEqual(result, {"sha": "new_sha"})
mock_request.assert_called_once()

@patch("challenges.github_interface.requests.request")
def test_update_content_from_path_failure(self, mock_request):
"""Test update_content_from_path with failed response returns None"""
from requests.exceptions import RequestException

@patch("challenges.github_utils.requests.put")
def test_update_text_file_failure(self, mock_put):
"""Test update_text_file with failure response"""
mock_response = Mock()
mock_response.status_code = 400
mock_put.return_value = mock_response
mock_response.raise_for_status.side_effect = RequestException()
mock_request.return_value = mock_response

result = self.github.update_text_file(
"test.json", "content", "message", "sha"
result = self.github.update_content_from_path(
"test.json", "Y29udGVudA==", changed_field="title"
)

self.assertIsNone(result)

@patch("challenges.github_utils.GithubInterface.get_file_contents")
@patch("challenges.github_utils.GithubInterface.update_text_file")
def test_update_json_if_changed_existing_changed(
self, mock_update_text, mock_get
):
"""When existing JSON differs, it should commit with updated content"""
old_data = {"a": 1}
old_text = json.dumps(old_data, sort_keys=True)
mock_get.return_value = {
"sha": "test_sha",
"content": old_text.encode("utf-8").decode("utf-8"),
} # content will be base64-decoded internally; provide as raw for simplicity
mock_update_text.return_value = {"sha": "new_sha"}

new_data = {"a": 2}
result = self.github.update_json_if_changed("test.json", new_data)

self.assertEqual(result, {"sha": "new_sha"})
mock_get.assert_called_once_with("test.json")
mock_update_text.assert_called_once()

@patch("challenges.github_utils.GithubInterface.get_file_contents")
@patch("challenges.github_utils.GithubInterface.update_text_file")
def test_update_json_if_changed_new_file(self, mock_update_text, mock_get):
"""When file doesn't exist, it should create it"""
mock_get.return_value = None
mock_update_text.return_value = {"sha": "new_sha"}

data = {"test": "data"}
result = self.github.update_json_if_changed("test.json", data)
@patch("challenges.github_interface.GithubInterface.update_content_from_path")
def test_update_data_from_path_encodes_and_calls_update(self, mock_update):
"""update_data_from_path should base64-encode and call update_content_from_path"""
mock_update.return_value = {"sha": "new_sha"}
text = "hello"
result = self.github.update_data_from_path("test.json", text, changed_field="title")

self.assertEqual(result, {"sha": "new_sha"})
mock_get.assert_called_once_with("test.json")
mock_update_text.assert_called_once()
mock_update.assert_called_once()


class TestGithubSync(TestCase):
"""Test cases for GitHub sync functionality"""

def setUp(self):
# Create a test challenge with GitHub configuration
self.user = User.objects.create(username="owner", email="o@example.com")
self.host_team = ChallengeHostTeam.objects.create(
team_name="team", created_by=self.user
)
self.challenge = Challenge.objects.create(
title="Test Challenge",
description="Test Description",
github_token="test_token",
github_repository="test/repo",
github_branch="master",
creator=self.host_team,
start_date=timezone.now(),
end_date=timezone.now() + timezone.timedelta(days=30),
)
Expand All @@ -153,90 +142,63 @@ def test_sync_challenge_to_github_success(self, mock_github_class):
"""Test successful challenge sync to GitHub"""
mock_github = Mock()
mock_github_class.return_value = mock_github
mock_github.update_json_if_changed.return_value = {"sha": "test_sha"}
mock_github.update_challenge_config.return_value = True

sync_challenge_to_github(self.challenge)
github_challenge_sync(self.challenge.id, changed_field="title")

mock_github_class.assert_called_once_with(
"test_token", "test/repo", "master"
)
mock_github.update_json_if_changed.assert_called_once()
mock_github_class.assert_called_once_with("test/repo", "master", "test_token")
mock_github.update_challenge_config.assert_called_once()

def test_sync_challenge_to_github_no_token(self):
"""Test challenge sync when no GitHub token is configured"""
self.challenge.github_token = ""

with self.assertLogs(level=logging.WARNING):
sync_challenge_to_github(self.challenge)
github_challenge_sync(self.challenge.id, changed_field="title")

def test_sync_challenge_to_github_no_repo(self):
"""Test challenge sync when no GitHub repository is configured"""
self.challenge.github_repository = ""

with self.assertLogs(level=logging.WARNING):
sync_challenge_to_github(self.challenge)
github_challenge_sync(self.challenge.id, changed_field="title")

@patch("challenges.github_utils.GithubInterface")
def test_sync_challenge_phase_to_github_success(self, mock_github_class):
"""Test successful challenge phase sync to GitHub"""
mock_github = Mock()
mock_github_class.return_value = mock_github
mock_github.update_json_if_changed.return_value = {"sha": "test_sha"}
mock_github.update_challenge_phase_config.return_value = True

sync_challenge_phase_to_github(self.challenge_phase)
github_challenge_phase_sync(self.challenge_phase.id, changed_field="name")

mock_github_class.assert_called_once_with(
"test_token", "test/repo", "master"
)
mock_github.update_json_if_changed.assert_called_once()
mock_github_class.assert_called_once_with("test/repo", "master", "test_token")
mock_github.update_challenge_phase_config.assert_called_once()

def test_sync_challenge_phase_to_github_no_token(self):
"""Test challenge phase sync when no GitHub token is configured"""
self.challenge.github_token = ""

with self.assertLogs(level=logging.WARNING):
sync_challenge_phase_to_github(self.challenge_phase)
github_challenge_phase_sync(self.challenge_phase.id, changed_field="name")

def test_sync_challenge_phase_to_github_no_repo(self):
"""Test challenge phase sync when no GitHub repository is configured"""
self.challenge.github_repository = ""

with self.assertLogs(level=logging.WARNING):
sync_challenge_phase_to_github(self.challenge_phase)
github_challenge_phase_sync(self.challenge_phase.id, changed_field="name")

@patch("challenges.github_utils.GithubInterface")
def test_sync_challenge_data_structure(self, mock_github_class):
"""Test that challenge data is properly structured for GitHub sync"""
def test_sync_challenge_calls_update(self, mock_github_class):
"""Basic check that challenge sync invokes update method"""
mock_github = Mock()
mock_github_class.return_value = mock_github

sync_challenge_to_github(self.challenge)

# Verify that update_data_from_path was called with challenge data
call_args = mock_github.update_json_if_changed.call_args
self.assertEqual(call_args[0][0], "challenge.json") # path
challenge_data = call_args[0][1] # data

# Check that key fields are present
self.assertEqual(challenge_data["title"], "Test Challenge")
self.assertEqual(challenge_data["description"], "Test Description")
self.assertIn("start_date", challenge_data)
self.assertIn("end_date", challenge_data)
github_challenge_sync(self.challenge.id, changed_field="title")
mock_github.update_challenge_config.assert_called_once()

@patch("challenges.github_utils.GithubInterface")
def test_sync_challenge_phase_data_structure(self, mock_github_class):
"""Test that challenge phase data is properly structured for GitHub sync"""
def test_sync_challenge_phase_calls_update(self, mock_github_class):
"""Basic check that challenge phase sync invokes update method"""
mock_github = Mock()
mock_github_class.return_value = mock_github

sync_challenge_phase_to_github(self.challenge_phase)

# Verify that update_data_from_path was called with phase data
call_args = mock_github.update_json_if_changed.call_args
self.assertEqual(call_args[0][0], "phases/test_phase.json") # path
phase_data = call_args[0][1] # data

# Check that key fields are present
self.assertEqual(phase_data["name"], "Test Phase")
self.assertEqual(phase_data["description"], "Test Phase Description")
self.assertEqual(phase_data["codename"], "test_phase")
github_challenge_phase_sync(self.challenge_phase.id, changed_field="name")
mock_github.update_challenge_phase_config.assert_called_once()