diff --git a/main.py b/main.py index a028a9d..96720d5 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,8 @@ from logging import basicConfig, INFO from readme_level import ReadmeLevel +DEFAULT_PROGRESS_BAR_CHAR_LENGTH: int = 30 + # set default config for application logging basicConfig( level=INFO, @@ -13,29 +15,42 @@ ) +def _get_progress_bar_length() -> int: + """Gets and validates the progress bar length from environment variables.""" + progress_bar_length: str | None = getenv("INPUT_PROGRESS_BAR_CHAR_LENGTH") + + if not progress_bar_length: + return DEFAULT_PROGRESS_BAR_CHAR_LENGTH + + try: + value: int = int(progress_bar_length) + except ValueError: + return DEFAULT_PROGRESS_BAR_CHAR_LENGTH + + return value if value > 0 else DEFAULT_PROGRESS_BAR_CHAR_LENGTH + + +def _env_is_truthy(var_name: str) -> bool: + """Converts common environment variable values to a boolean.""" + return getenv(var_name, "").lower() in {"1", "true", "yes", "on"} + + def draw_progress_bar(current_progress: float | int) -> str: """Draws the progress bar""" - progress_bar_length: int = int(getenv("INPUT_PROGRESS_BAR_CHAR_LENGTH")) + progress_bar_length: int = _get_progress_bar_length() + current_progress = max(0.0, min(100.0, float(current_progress))) progress_bar_content = { "empty_bar": getenv("INPUT_EMPTY_BAR"), "filled_bar": getenv("INPUT_FILLED_BAR") } - progress_bar: str = "" filled_progress: int = round( progress_bar_length * (current_progress / 100), 0) - - for index in range(progress_bar_length): - - # because the index starts at 0 we reduce filled_progress - # we should define our own index - if index <= filled_progress - 1: - progress_bar += progress_bar_content["filled_bar"] - - if index > filled_progress - 1: - progress_bar += progress_bar_content["empty_bar"] - + progress_bar: str = ( + progress_bar_content["filled_bar"] * int(filled_progress) + + progress_bar_content["empty_bar"] * (progress_bar_length - int(filled_progress)) + ) return progress_bar @@ -58,7 +73,7 @@ def generate_content(readme_instance: ReadmeLevel, start_section: str, end_secti f"{ getenv('INPUT_CARD_TITLE') if getenv('INPUT_CARD_TITLE') else '' } \n" f"
level: { user_level } \
 { draw_progress_bar(to_next_lvl) } {round(to_next_lvl, 2)}%
\n" - f"{ ep_information if getenv('INPUT_SHOW_EP_INFO') else '' }" + f"{ ep_information if _env_is_truthy('INPUT_SHOW_EP_INFO') else '' }" f"{end_section}") diff --git a/readme_level.py b/readme_level.py index 81f82da..8b5bbfe 100644 --- a/readme_level.py +++ b/readme_level.py @@ -1,8 +1,9 @@ """Module that contains all the logic about the levelsystem.""" from os import getenv from datetime import datetime -from logging import exception, info +from logging import exception, info, error from requests import post +from requests.exceptions import RequestException from graphql_query import REQUEST_QUERY from readme_data import ReadmeLevelData @@ -22,22 +23,32 @@ def __init__(self) -> None: def fetch_user_data(self) -> dict[str, int] | None: """Fetches the user data from github api""" - if not getenv("INPUT_GITHUB_TOKEN"): - exception("an error with the github token occurred") + github_token: str | None = getenv("INPUT_GITHUB_TOKEN") + if not github_token: + error("missing github token") + return None - auth_header = {"Authorization": "Bearer " + - getenv("INPUT_GITHUB_TOKEN")} - response = post("https://api.github.com/graphql", - json={"query": REQUEST_QUERY}, headers=auth_header, timeout=2) + auth_header = {"Authorization": "Bearer " + github_token} - if response.status_code == 200: - info("request to github api was successfull") + try: + response = post("https://api.github.com/graphql", + json={"query": REQUEST_QUERY}, headers=auth_header, timeout=2) + except RequestException: + exception("request to github api failed") + return None - response_data = response.json() + if response.status_code != 200: + error("request to github api failed with status code %s", response.status_code) + return None - current_year = datetime.now().year - total_contribution = [] + info("request to github api was successful") + response_data = response.json() + + current_year = datetime.now().year + total_contribution = [] + + try: while current_year >= 2015: contribution_count = (response_data["data"]["user"] ["_" + str(current_year)]["contributionCalendar"]["totalContributions"]) @@ -45,7 +56,6 @@ def fetch_user_data(self) -> dict[str, int] | None: total_contribution.append(contribution_count) current_year -= 1 - user_data = {} user_data["totalContributions"] = sum(total_contribution) @@ -54,18 +64,21 @@ def fetch_user_data(self) -> dict[str, int] | None: ["followers"]["totalCount"]) user_data["totalRepositories"] = (response_data["data"]["user"] - ["repositories"]["totalCount"]) - - return user_data + ["repositories"]["totalCount"]) + except (KeyError, TypeError): + exception("github api response is malformed") + return None - exception("request to github api failed") - return None + return user_data def calc_current_ep(self) -> int: """Calculates the current user experience points""" # get user stats - user_stats: dict[str, int] = self.fetch_user_data() + user_stats: dict[str, int] | None = self.fetch_user_data() + if user_stats is None: + self.current_ep = 0 + return self.current_ep # calc the current experience points self.current_ep = ( diff --git a/tests/test_main.py b/tests/test_main.py index ae8bf0b..271292c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -60,9 +60,28 @@ def test_generate_content(self) -> None: mock_readme_instance, start_section, end_section) # we check only if return value is from type str because the spaces makes - # it realy difficult to check for isEqual + # it really difficult to check for isEqual self.assertIsInstance(replace_str, str) + @patch.dict(os.environ, {'INPUT_SHOW_EP_INFO': 'false'}, clear=True) + def test_generate_content_without_ep_info(self) -> None: + """Tests generated content without ep information.""" + mock_readme_instance = MagicMock() + mock_readme_instance.get_current_level.return_value = { + "current_level": "10", "percentage_level": 20} + mock_readme_instance.get_contribution_ep = 20 + mock_readme_instance.get_follower_ep = 20 + mock_readme_instance.get_project_ep = 5 + + with patch('main.draw_progress_bar', return_value="███"): + replace_str: str = generate_content( + mock_readme_instance, + "", + "" + ) + + self.assertNotIn("experience points", replace_str) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_readme_level.py b/tests/test_readme_level.py index af7b756..2094f3b 100644 --- a/tests/test_readme_level.py +++ b/tests/test_readme_level.py @@ -2,8 +2,9 @@ Unit Tests for readme_level.py """ +import os import unittest -from unittest.mock import patch +from unittest.mock import patch, MagicMock from readme_level import ReadmeLevel class TestLevelSystem(unittest.TestCase): @@ -37,6 +38,36 @@ def test_calc_current_ep(self, mock_fetch_user_data): readme_instance.calc_current_ep() self.assertEqual(readme_instance.current_ep, 2725) + @patch('readme_level.ReadmeLevel.fetch_user_data') + def test_calc_current_ep_without_user_data(self, mock_fetch_user_data): + """Tests ep calc with missing user data.""" + mock_fetch_user_data.return_value = None + + readme_instance: ReadmeLevel = ReadmeLevel() + readme_instance.calc_current_ep() + self.assertEqual(readme_instance.current_ep, 0) + + @patch.dict(os.environ, {}, clear=True) + @patch('readme_level.post') + def test_fetch_user_data_without_token(self, mock_post): + """Tests fetch_user_data returns None if token is missing.""" + readme_instance: ReadmeLevel = ReadmeLevel() + user_data = readme_instance.fetch_user_data() + + self.assertIsNone(user_data) + mock_post.assert_not_called() + + @patch.dict(os.environ, {"INPUT_GITHUB_TOKEN": "test-token"}, clear=True) + @patch('readme_level.post') + def test_fetch_user_data_unsuccessful_response(self, mock_post): + """Tests fetch_user_data handles non-200 responses.""" + mock_post.return_value = MagicMock(status_code=500) + + readme_instance: ReadmeLevel = ReadmeLevel() + user_data = readme_instance.fetch_user_data() + + self.assertIsNone(user_data) + if __name__ == '__main__': unittest.main()