Skip to content

Commit

Permalink
Feature/34 introduce unit tests for model folder (#42)
Browse files Browse the repository at this point in the history
* Unit tests using Pytest for model folder
  • Loading branch information
MobiTikula authored Oct 16, 2024
1 parent d1ac0f6 commit 8799012
Show file tree
Hide file tree
Showing 15 changed files with 429 additions and 70 deletions.
6 changes: 4 additions & 2 deletions living_documentation_generator/action_inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ def load_from_environment(self, validate: bool = True) -> "ActionInputs":

for repository_json in repositories_json:
config_repository = ConfigRepository()
config_repository.load_from_json(repository_json)
self.__repositories.append(config_repository)
if config_repository.load_from_json(repository_json):
self.__repositories.append(config_repository)
else:
logger.error("Failed to load repository from JSON: %s.", repository_json)

return self

Expand Down
22 changes: 16 additions & 6 deletions living_documentation_generator/model/config_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
This module contains a data container for Config Repository, which holds all the essential logic.
"""

import logging
from typing import Optional

logger = logging.getLogger(__name__)


class ConfigRepository:
"""
Expand Down Expand Up @@ -53,14 +56,21 @@ def projects_title_filter(self) -> list[str]:
"""Getter of the project title filter."""
return self.__projects_title_filter

def load_from_json(self, repository_json: dict) -> None:
def load_from_json(self, repository_json: dict) -> bool:
"""
Load the configuration from a JSON object.
@param repository_json: The JSON object containing the repository configuration.
@return: None
@return: bool
"""
self.__organization_name = repository_json["organization-name"]
self.__repository_name = repository_json["repository-name"]
self.__query_labels = repository_json["query-labels"]
self.__projects_title_filter = repository_json["projects-title-filter"]
try:
self.__organization_name = repository_json["organization-name"]
self.__repository_name = repository_json["repository-name"]
self.__query_labels = repository_json["query-labels"]
self.__projects_title_filter = repository_json["projects-title-filter"]
return True
except KeyError as e:
logger.error("The key is not found in the repository JSON input: %s.", e, exc_info=True)
except TypeError as e:
logger.error("The repository JSON input does not have a dictionary structure: %s.", e, exc_info=True)
return False
18 changes: 15 additions & 3 deletions living_documentation_generator/model/consolidated_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
"""
This module contains a data container for Consolidated Issue, which holds all the essential logic.
"""

import logging
from typing import Optional

from github.Issue import Issue

from living_documentation_generator.utils.utils import sanitize_filename
from living_documentation_generator.model.project_status import ProjectStatus

logger = logging.getLogger(__name__)


class ConsolidatedIssue:
"""
Expand Down Expand Up @@ -145,7 +147,17 @@ def generate_page_filename(self) -> str:
@return: The generated page filename.
"""
md_filename_base = f"{self.number}_{self.title.lower()}.md"
page_filename = sanitize_filename(md_filename_base)
try:
md_filename_base = f"{self.number}_{self.title.lower()}.md"
page_filename = sanitize_filename(md_filename_base)
except AttributeError:
logger.error(
"Issue page filename generation failed for Issue %s/%s (%s). Issue does not have a title.",
self.organization_name,
self.repository_name,
self.number,
exc_info=True,
)
return f"{self.number}.md"

return page_filename
24 changes: 17 additions & 7 deletions living_documentation_generator/model/github_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,27 @@ def loads(self, project_json: dict, repository: Repository, field_option_respons
@param field_option_response: The response containing the field options for the project.
@return: The GithubProject object with the loaded data.
"""
self.__id = project_json["id"]
self.__number = project_json["number"]
self.__title = project_json["title"]
self.__organization_name = repository.owner.login
try:
self.__id = project_json["id"]
self.__number = project_json["number"]
self.__title = project_json["title"]
self.__organization_name = repository.owner.login

logger.debug("Updating field options for projects in repository `%s`.", repository.full_name)
except KeyError as e:
logger.error(
"Missing key in the project json for repository `%s`: %s",
repository.full_name,
str(e),
exc_info=True,
)
return self

logger.debug("Updating field options for projects in repository `%s`.", repository.full_name)
self.__update_field_options(field_option_response)
self._update_field_options(field_option_response)

return self

def __update_field_options(self, field_option_response: dict) -> None:
def _update_field_options(self, field_option_response: dict) -> None:
"""
Parse and update the field options of the project from a JSON response.
Expand Down
15 changes: 9 additions & 6 deletions living_documentation_generator/model/project_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,17 @@ def loads(self, issue_json: dict, project: GithubProject) -> Optional["ProjectIs
@param: project: The GithubProject object representing the project the issue belongs to.
@return: The ProjectIssue object with the loaded data.
"""
if not issue_json["content"]:
logger.debug("No issue data provided in received json.")
logger.debug(issue_json)
if "content" not in issue_json:
logger.debug("No issue data provided in received json: %s.", issue_json)
return None

self.__number = issue_json["content"]["number"]
self.__organization_name = issue_json["content"]["repository"]["owner"]["login"]
self.__repository_name = issue_json["content"]["repository"]["name"]
try:
self.__number = issue_json["content"]["number"]
self.__repository_name = issue_json["content"]["repository"]["name"]
self.__organization_name = issue_json["content"]["repository"]["owner"]["login"]
except KeyError as e:
logger.debug("KeyError(%s) occurred while parsing issue json: %s.", str(e), issue_json)

self.__project_status.project_title = project.title

# Parse the field types from the response
Expand Down
18 changes: 18 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
from github import Github
from github.Rate import Rate
from github.RateLimit import RateLimit
from github.Repository import Repository
from pytest_mock import mocker

from living_documentation_generator.model.github_project import GithubProject
from living_documentation_generator.utils.github_rate_limiter import GithubRateLimiter


Expand Down Expand Up @@ -48,3 +51,18 @@ def mock_rate_limiter(mocker):
def mock_logging_setup(mocker):
mock_log_config = mocker.patch("logging.basicConfig")
yield mock_log_config


@pytest.fixture
def github_project_setup(mocker):
project = mocker.Mock(spec=GithubProject)
project_json = {"id": "123", "number": 1, "title": "Test Project"}

repository = mocker.Mock(spec=Repository)
repository.owner.login = "organizationABC"
repository.full_name = "organizationABC/repoABC"

field_option_response = {}
project.loads(project_json, repository, field_option_response)

return project
67 changes: 67 additions & 0 deletions tests/model/test_config_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#
# Copyright 2024 ABSA Group Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from living_documentation_generator.model.config_repository import ConfigRepository


def test_load_from_json_with_valid_input_loads_correctly():
config_repository = ConfigRepository()
organization_name = "organizationABC"
repository_name = "repositoryABC"
query_labels = ["feature", "bug"]
projects_title_filter = ["project1"]
other_value = "other-value"
repository_json = {
"organization-name": organization_name,
"repository-name": repository_name,
"query-labels": query_labels,
"projects-title-filter": projects_title_filter,
"other-field": other_value,
}

actual = config_repository.load_from_json(repository_json)

assert actual
assert organization_name == config_repository.organization_name
assert repository_name == config_repository.repository_name
assert query_labels == config_repository.query_labels
assert projects_title_filter == config_repository.projects_title_filter


def test_load_from_json_with_missing_key_logs_error(mocker):
config_repository = ConfigRepository()
mock_log_error = mocker.patch("living_documentation_generator.model.config_repository.logger.error")
repository_json = {"non-existent-key": "value"}

actual = config_repository.load_from_json(repository_json)

assert actual is False
mock_log_error.assert_called_once_with(
"The key is not found in the repository JSON input: %s.", mocker.ANY, exc_info=True
)


def test_load_from_json_with_wrong_structure_input_logs_error(mocker):
config_repository = ConfigRepository()
mock_log_error = mocker.patch("living_documentation_generator.model.config_repository.logger.error")
repository_json = "not a dictionary"

actual = config_repository.load_from_json(repository_json)

assert actual is False
mock_log_error.assert_called_once_with(
"The repository JSON input does not have a dictionary structure: %s.", mocker.ANY, exc_info=True
)
47 changes: 47 additions & 0 deletions tests/model/test_consolidated_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#
# Copyright 2024 ABSA Group Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
from github.Issue import Issue

from living_documentation_generator.model.consolidated_issue import ConsolidatedIssue


# generate_page_filename


def test_generate_page_filename_correct_behaviour():
mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)

actual = consolidated_issue.generate_page_filename()

assert "1_issue_title.md" == actual


def test_generate_page_filename_with_none_title(mocker):
mock_log_error = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.error")
mock_issue = Issue(None, None, {"number": 1, "title": None}, completed=True)
consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)

actual = consolidated_issue.generate_page_filename()

assert "1.md" == actual
mock_log_error.assert_called_once_with(
"Issue page filename generation failed for Issue %s/%s (%s). Issue does not have a title.",
"organization",
"repository",
1,
exc_info=True,
)
Loading

0 comments on commit 8799012

Please sign in to comment.