Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
80 changes: 51 additions & 29 deletions sync2jira/upstream_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,55 @@
"""


def passes_github_filters(item, config, upstream, item_type="issue"):
"""
Apply GitHub filters (labels, milestone, other fields) to an item.

:param dict item: GitHub issue or PR data
:param dict _filter: Filter configuration
:param str upstream: Upstream repository name
:param str item_type: Type of item for logging ("issue" or "PR")
:returns: True if item passes all filters, False otherwise
:rtype: bool
"""
filter_config = (
config["sync2jira"].get("filters", {}).get("github", {}).get(upstream, {})
)

for key, expected in filter_config.items():
if key == "labels":
# special handling for label: we look for it in the list of labels
actual = {label["name"] for label in item.get("labels", [])}
if actual.isdisjoint(expected):
log.debug(
"Labels %s not found on %s: %s", expected, upstream, item_type
)
return False
elif key == "milestone":
# special handling for milestone: use the number
milestone = item.get(key) or {} # Key might exist with value `None`
actual = milestone.get("number")
if expected != actual:
log.debug(
"Milestone %s not set on %s: %s", expected, upstream, item_type
)
return False
else:
# direct comparison
actual = item.get(key)
if actual != expected:
log.debug(
"Actual %r %r != expected %r on %s %s",
key,
actual,
expected,
upstream,
item_type,
)
return False
return True


def handle_github_message(body, config, is_pr=False):
"""
Handle GitHub message from FedMsg.
Expand Down Expand Up @@ -137,36 +186,9 @@ def handle_github_message(body, config, is_pr=False):
)
return None

_filter = config["sync2jira"].get("filters", {}).get("github", {}).get(upstream, {})

issue = body["issue"]
for key, expected in _filter.items():
if key == "labels":
# special handling for label: we look for it in the list of msg labels
actual = {label["name"] for label in issue["labels"]}
if actual.isdisjoint(expected):
log.debug("Labels %s not found on issue: %s", expected, upstream)
return None
elif key == "milestone":
# special handling for milestone: use the number
milestone = issue.get(key) or {} # Key might exist with value `None`
actual = milestone.get("number")
if expected != actual:
log.debug("Milestone %s not set on issue: %s", expected, upstream)
return None
else:
# direct comparison
actual = issue.get(key)
if actual != expected:
log.debug(
"Actual %r %r != expected %r on issue %s",
key,
actual,
expected,
upstream,
)
return None

if not passes_github_filters(issue, config, upstream, "issue"):
return None
if is_pr and not issue.get("closed_at"):
log.debug(
"%r is a pull request. Ignoring.", issue.get("html_url", "<missing URL>")
Expand Down
2 changes: 2 additions & 0 deletions sync2jira/upstream_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def handle_github_message(body, config, suffix):
return None

pr = body["pull_request"]
if not u_issue.passes_github_filters(pr, config, upstream, item_type="PR"):
return None
github_client = Github(config["sync2jira"]["github_token"])
reformat_github_pr(pr, upstream, github_client)
return i.PR.from_github(upstream, pr, suffix, config)
Expand Down
48 changes: 48 additions & 0 deletions tests/test_upstream_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -767,3 +767,51 @@ def test_add_project_values_early_exit(self, mock_requests_post):
self.assertIsNone(result)
# Reset mock
mock_requests_post.reset_mock()

def test_passes_github_filters(self):
"""
Test passes_github_filters for labels, milestone, and other fields.
Tests all filtering conditions in one test case.
"""
self.mock_config["sync2jira"]["filters"]["github"]["org/repo"]["milestone"] = 1
upstream = "org/repo"

# Test 1: Bad label - should return False
item = {
"labels": [{"name": "bad_label"}],
"milestone": {"number": 1},
"filter1": "filter1",
}
self.assertFalse(
u.passes_github_filters(item, self.mock_config, upstream, item_type="issue")
)

# Test 2: Bad milestone - should return False
item = {
"labels": [{"name": "custom_tag"}],
"milestone": {"number": 456},
"filter1": "filter1",
}
self.assertFalse(
u.passes_github_filters(item, self.mock_config, upstream, item_type="issue")
)

# Test 3: Bad other field (filter1) - should return False
item = {
"labels": [{"name": "custom_tag"}],
"milestone": {"number": 1},
"filter1": "filter2",
}
self.assertFalse(
u.passes_github_filters(item, self.mock_config, upstream, item_type="issue")
)

# Test 4: All filters pass - should return True
item = {
"labels": [{"name": "custom_tag"}],
"milestone": {"number": 1},
"filter1": "filter1",
}
self.assertTrue(
u.passes_github_filters(item, self.mock_config, upstream, item_type="issue")
)
131 changes: 131 additions & 0 deletions tests/test_upstream_pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,134 @@ def test_filter_multiple_labels(
self.mock_config["sync2jira"]["filters"]["github"]["org/repo"]["labels"],
["custom_tag", "another_tag", "and_another"],
)

@mock.patch("sync2jira.intermediary.PR.from_github")
def test_handle_github_message_filtering(self, mock_pr_from_github):
"""
Test 'handle_github_message' filtering for labels, milestone, and other fields.
Tests all three filtering conditions in one test case.
"""
# Test 1: Bad label - PR should be filtered out
self.mock_github_message_body["pull_request"]["labels"] = [
{"name": "bad_label"}
]

response = u.handle_github_message(
body=self.mock_github_message_body,
config=self.mock_config,
suffix="mock_suffix",
)

mock_pr_from_github.assert_not_called()
self.assertEqual(None, response)

# Reset for next test
mock_pr_from_github.reset_mock()
self.mock_github_message_body["pull_request"]["labels"] = [
{"name": "custom_tag"}
]

# Test 2: Bad milestone - PR should be filtered out
self.mock_config["sync2jira"]["filters"]["github"]["org/repo"][
"milestone"
] = 123
self.mock_github_message_body["pull_request"]["milestone"] = {"number": 456}

response = u.handle_github_message(
body=self.mock_github_message_body,
config=self.mock_config,
suffix="mock_suffix",
)

mock_pr_from_github.assert_not_called()
self.assertEqual(None, response)

# Reset for next test
mock_pr_from_github.reset_mock()
del self.mock_config["sync2jira"]["filters"]["github"]["org/repo"]["milestone"]
self.mock_github_message_body["pull_request"]["milestone"] = {
"title": "mock_milestone"
}

# Test 3: Bad other field (filter1) - PR should be filtered out
self.mock_github_message_body["pull_request"]["filter1"] = "filter2"

response = u.handle_github_message(
body=self.mock_github_message_body,
config=self.mock_config,
suffix="mock_suffix",
)

mock_pr_from_github.assert_not_called()
self.assertEqual(None, response)

@mock.patch(PATH + "Github")
@mock.patch("sync2jira.intermediary.PR.from_github")
def test_handle_github_message_filtering_passes(
self, mock_pr_from_github, mock_github
):
"""
Test 'handle_github_message' when all filters pass (labels, milestone, other fields).
"""
# Set up filters with all three types
self.mock_config["sync2jira"]["filters"]["github"]["org/repo"]["milestone"] = 1
self.mock_github_message_body["pull_request"]["milestone"] = {
"number": 1,
"title": "mock_milestone",
}

# Set up return values
mock_pr_from_github.return_value = "Successful Call!"
mock_github.return_value = self.mock_github_client

# Call function
response = u.handle_github_message(
body=self.mock_github_message_body,
config=self.mock_config,
suffix="mock_suffix",
)

# Assert that PR was processed (all filters passed)
mock_pr_from_github.assert_called_once()
mock_github.assert_called_with("mock_token")
self.assertEqual("Successful Call!", response)

@mock.patch("sync2jira.upstream_pr.u_issue.passes_github_filters")
@mock.patch("sync2jira.intermediary.PR.from_github")
def test_handle_github_message_filter_returns_false(
self, mock_pr_from_github, mock_passes
):
"""When passes_github_filters returns False, handle_github_message returns None."""
mock_passes.return_value = False

response = u.handle_github_message(
body=self.mock_github_message_body,
config=self.mock_config,
suffix="mock_suffix",
)

mock_passes.assert_called_once()
mock_pr_from_github.assert_not_called()
self.assertIsNone(response)

@mock.patch(PATH + "Github")
@mock.patch("sync2jira.upstream_pr.u_issue.passes_github_filters")
@mock.patch("sync2jira.intermediary.PR.from_github")
def test_handle_github_message_filter_returns_true(
self, mock_pr_from_github, mock_passes, mock_github
):
"""When passes_github_filters returns True, handle_github_message proceeds to PR.from_github."""
mock_passes.return_value = True
mock_pr_from_github.return_value = "Successful Call!"
mock_github.return_value = self.mock_github_client

response = u.handle_github_message(
body=self.mock_github_message_body,
config=self.mock_config,
suffix="mock_suffix",
)

mock_passes.assert_called_once()
mock_pr_from_github.assert_called_once()
mock_github.assert_called_with("mock_token")
self.assertEqual("Successful Call!", response)