Add pagure_fas_groups_sync toddler
This toddler is syncing configured groups between FAS and pagure.io. Signed-off-by: Michal Konecny <mkonecny@redhat.com>
This commit is contained in:
parent
cc277eaaf8
commit
f615451f0c
7 changed files with 1026 additions and 2 deletions
18
docs/pagure_fas_groups_sync.md
Normal file
18
docs/pagure_fas_groups_sync.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Sync Groups From FAS to Pagure
|
||||
|
||||
Toddler for synchronizing FAS groups with Pagure groups was created from https://pagure.io/pagure-utility/blob/master/f/sync_fas_group_membership.py script. It is triggered either by group membership messages or by `toddlers.trigger` message.
|
||||
|
||||
In case of group membership message it works as following:
|
||||
|
||||
1. Check if we care about the group (it's in configuration for toddler)
|
||||
1. Add member from group (currently there is no message about user being removed from group)
|
||||
|
||||
In case of `toddlers.trigger` message it compares all the groups in configuration
|
||||
and remove/add users to pagure group based on the changes in FAS.
|
||||
|
||||
## Accepted topics
|
||||
|
||||
The sync toddler accepts following topics.
|
||||
|
||||
* org.fedoraproject.*.fas.group.member.sponsor - New member added to group
|
||||
* org.fedoraproject.*.toddlers.trigger.pagure_fas_groups_sync - Message triggered by toddlers cron
|
|
@ -8,6 +8,7 @@ fedora-messaging-git-hook-messages
|
|||
GitPython
|
||||
koji
|
||||
requests
|
||||
noggin-messages
|
||||
pagure-messages
|
||||
pyGObject
|
||||
python-fedora
|
||||
|
|
409
tests/plugins/test_pagure_fas_groups_sync.py
Normal file
409
tests/plugins/test_pagure_fas_groups_sync.py
Normal file
|
@ -0,0 +1,409 @@
|
|||
"""
|
||||
Unit tests for `toddlers.plugins.scm_request_processor`
|
||||
"""
|
||||
|
||||
import logging
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from fedora_messaging.api import Message
|
||||
from noggin_messages import MemberSponsorV1
|
||||
import pytest
|
||||
|
||||
from toddlers.exceptions import PagureError
|
||||
from toddlers.plugins import pagure_fas_groups_sync
|
||||
|
||||
|
||||
class TestAcceptsTopic:
|
||||
"""
|
||||
Test class for `toddlers.plugins.pagure_fas_groups_sync.PagureFASGroupsSync.accepts_topic`
|
||||
method.
|
||||
"""
|
||||
|
||||
toddler_cls = pagure_fas_groups_sync.PagureFASGroupsSync
|
||||
|
||||
def test_accepts_topic_invalid(self, toddler):
|
||||
"""
|
||||
Assert that invalid topic is not accepted.
|
||||
"""
|
||||
assert toddler.accepts_topic("foo.bar") is False
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"topic",
|
||||
[
|
||||
"org.fedoraproject.*.fas.group.member.sponsor",
|
||||
"org.fedoraproject.*.toddlers.trigger.pagure_fas_groups_sync",
|
||||
"org.fedoraproject.stg.fas.group.member.sponsor",
|
||||
"org.fedoraproject.stg.toddlers.trigger.pagure_fas_groups_sync",
|
||||
"org.fedoraproject.prod.fas.group.member.sponsor",
|
||||
"org.fedoraproject.prod.toddlers.trigger.pagure_fas_groups_sync",
|
||||
],
|
||||
)
|
||||
def test_accepts_topic_valid(self, topic, toddler):
|
||||
"""
|
||||
Assert that valid topics are accepted.
|
||||
"""
|
||||
assert toddler.accepts_topic(topic)
|
||||
|
||||
|
||||
class TestProcess:
|
||||
"""
|
||||
Test class for `toddlers.plugins.pagure_fas_groups_sync.PagureFASGroupsSync.process` method.
|
||||
"""
|
||||
|
||||
toddler_cls = pagure_fas_groups_sync.PagureFASGroupsSync
|
||||
|
||||
@patch("toddlers.utils.pagure.set_pagure")
|
||||
@patch("toddlers.utils.fedora_account.set_fasjson")
|
||||
def test_process_trigger_message(self, mock_fas, mock_pagure, toddler):
|
||||
"""
|
||||
Assert that trigger message is processed correctly.
|
||||
"""
|
||||
# Preparation
|
||||
config = {"group_map": {"fas_group": "pagure_group"}}
|
||||
msg = Message()
|
||||
msg.topic = "org.fedoraproject.prod.toddlers.trigger.pagure_fas_groups_sync"
|
||||
|
||||
# Test
|
||||
with patch(
|
||||
"toddlers.plugins.pagure_fas_groups_sync.PagureFASGroupsSync.sync_group"
|
||||
) as mock_sync_group:
|
||||
toddler.process(config, msg)
|
||||
|
||||
# Assertions
|
||||
mock_sync_group.assert_called_with("fas_group", "pagure_group")
|
||||
|
||||
mock_fas.assert_called_with(config)
|
||||
mock_pagure.assert_called_with(config)
|
||||
|
||||
@patch("toddlers.utils.pagure.set_pagure")
|
||||
@patch("toddlers.utils.fedora_account.set_fasjson")
|
||||
def test_process_sponsor_message(self, mock_fas, mock_pagure, toddler):
|
||||
"""
|
||||
Assert that sponsor message is processed correctly.
|
||||
"""
|
||||
# Preparation
|
||||
mock_pagure_obj = Mock()
|
||||
mock_pagure.return_value = mock_pagure_obj
|
||||
config = {"group_map": {"fas_group": "pagure_group"}}
|
||||
msg = MemberSponsorV1(
|
||||
{"msg": {"agent": "agent", "user": "user", "group": "fas_group"}}
|
||||
)
|
||||
|
||||
# Test
|
||||
toddler.process(config, msg)
|
||||
|
||||
# Assertions
|
||||
mock_pagure_obj.add_member_to_group.assert_called_with("user", "pagure_group")
|
||||
|
||||
mock_fas.assert_called_with(config)
|
||||
mock_pagure.assert_called_with(config)
|
||||
|
||||
@patch("toddlers.utils.pagure.set_pagure")
|
||||
@patch("toddlers.utils.fedora_account.set_fasjson")
|
||||
def test_process_sponsor_message_user_not_in_pagure(
|
||||
self, mock_fas, mock_pagure, toddler
|
||||
):
|
||||
"""
|
||||
Assert that sponsor message is processed correctly when user
|
||||
doesn't exist in pagure.
|
||||
"""
|
||||
# Preparation
|
||||
mock_pagure_obj = Mock()
|
||||
mock_pagure_obj.user_exists.return_value = False
|
||||
mock_pagure.return_value = mock_pagure_obj
|
||||
config = {"group_map": {"fas_group": "pagure_group"}}
|
||||
msg = MemberSponsorV1(
|
||||
{"msg": {"agent": "agent", "user": "user", "group": "fas_group"}}
|
||||
)
|
||||
|
||||
# Test
|
||||
toddler.process(config, msg)
|
||||
|
||||
# Assertions
|
||||
mock_pagure_obj.add_member_to_group.assert_not_called()
|
||||
|
||||
mock_fas.assert_called_with(config)
|
||||
mock_pagure.assert_called_with(config)
|
||||
|
||||
@patch("toddlers.utils.pagure.set_pagure")
|
||||
@patch("toddlers.utils.fedora_account.set_fasjson")
|
||||
def test_process_sponsor_message_failure(
|
||||
self, mock_fas, mock_pagure, caplog, toddler
|
||||
):
|
||||
"""
|
||||
Assert that failure during processing sponsor message is handled correctly.
|
||||
"""
|
||||
# Preparation
|
||||
caplog.set_level(logging.ERROR)
|
||||
mock_pagure_obj = Mock()
|
||||
mock_pagure_obj.add_member_to_group.side_effect = PagureError("PagureError")
|
||||
mock_pagure.return_value = mock_pagure_obj
|
||||
config = {"group_map": {"fas_group": "pagure_group"}}
|
||||
msg = MemberSponsorV1(
|
||||
{"msg": {"agent": "agent", "user": "user", "group": "fas_group"}}
|
||||
)
|
||||
|
||||
# Test
|
||||
toddler.process(config, msg)
|
||||
|
||||
# Assertions
|
||||
mock_pagure_obj.add_member_to_group.assert_called_with("user", "pagure_group")
|
||||
|
||||
mock_fas.assert_called_with(config)
|
||||
mock_pagure.assert_called_with(config)
|
||||
|
||||
assert caplog.records[-1].message == "PagureError"
|
||||
|
||||
@patch("toddlers.utils.pagure.set_pagure")
|
||||
@patch("toddlers.utils.fedora_account.set_fasjson")
|
||||
def test_process_sponsor_message_skipping(
|
||||
self, mock_fas, mock_pagure, caplog, toddler
|
||||
):
|
||||
"""
|
||||
Assert that group is skipped when we don't care about it.
|
||||
"""
|
||||
# Preparation
|
||||
caplog.set_level(logging.INFO)
|
||||
mock_pagure_obj = Mock()
|
||||
mock_pagure.return_value = mock_pagure_obj
|
||||
config = {"group_map": {"fas_group": "pagure_group"}}
|
||||
msg = MemberSponsorV1(
|
||||
{"msg": {"agent": "agent", "user": "user", "group": "some_group"}}
|
||||
)
|
||||
|
||||
# Test
|
||||
toddler.process(config, msg)
|
||||
|
||||
# Assertions
|
||||
mock_pagure_obj.add_member_to_group.assert_not_called()
|
||||
|
||||
mock_fas.assert_called_with(config)
|
||||
mock_pagure.assert_called_with(config)
|
||||
|
||||
assert caplog.records[-1].message.startswith(
|
||||
"User 'user' was added to group 'some_group'"
|
||||
)
|
||||
|
||||
|
||||
class TestSyncGroup:
|
||||
"""
|
||||
Test class for `toddlers.plugins.pagure_fas_groups_sync.sync_group` function.
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Prepare the toddler object."""
|
||||
self.sync_object = pagure_fas_groups_sync.PagureFASGroupsSync()
|
||||
self.sync_object.pagure = Mock()
|
||||
|
||||
@patch("toddlers.plugins.pagure_fas_groups_sync.fedora_account")
|
||||
def test_sync_group_add_member(self, mock_fas):
|
||||
"""Assert that adding member to group works as intended."""
|
||||
# Preparation
|
||||
fas_group = "fas_group"
|
||||
pagure_group = "pagure_group"
|
||||
self.sync_object.pagure.get_group_members.return_value = ["user1"]
|
||||
mock_fas.get_group_member.return_value = ["user1", "user2"]
|
||||
|
||||
# Test
|
||||
self.sync_object.sync_group(fas_group, pagure_group)
|
||||
|
||||
# Assertions
|
||||
mock_fas.get_group_member.assert_called_with(fas_group)
|
||||
self.sync_object.pagure.get_group_members.assert_called_with(pagure_group)
|
||||
|
||||
self.sync_object.pagure.add_member_to_group.assert_called_with(
|
||||
"user2", pagure_group
|
||||
)
|
||||
self.sync_object.pagure.remove_member_from_group.assert_not_called()
|
||||
|
||||
@patch("toddlers.plugins.pagure_fas_groups_sync.fedora_account")
|
||||
def test_sync_group_add_member_user_not_in_pagure(self, mock_fas):
|
||||
"""
|
||||
Assert that adding member to group works as intended
|
||||
when the user doesn't exist in pagure.
|
||||
"""
|
||||
# Preparation
|
||||
fas_group = "fas_group"
|
||||
pagure_group = "pagure_group"
|
||||
self.sync_object.pagure.get_group_members.return_value = ["user1"]
|
||||
self.sync_object.pagure.user_exists.return_value = False
|
||||
mock_fas.get_group_member.return_value = ["user1", "user2"]
|
||||
|
||||
# Test
|
||||
self.sync_object.sync_group(fas_group, pagure_group)
|
||||
|
||||
# Assertions
|
||||
mock_fas.get_group_member.assert_called_with(fas_group)
|
||||
self.sync_object.pagure.get_group_members.assert_called_with(pagure_group)
|
||||
|
||||
self.sync_object.pagure.add_member_to_group.assert_not_called()
|
||||
self.sync_object.pagure.remove_member_from_group.assert_not_called()
|
||||
|
||||
@patch("toddlers.plugins.pagure_fas_groups_sync.fedora_account")
|
||||
def test_sync_group_add_member_failure(self, mock_fas, caplog):
|
||||
"""Assert that failing to adding member to group works as intended."""
|
||||
# Preparation
|
||||
caplog.set_level(logging.ERROR)
|
||||
fas_group = "fas_group"
|
||||
pagure_group = "pagure_group"
|
||||
self.sync_object.pagure.get_group_members.return_value = ["user1"]
|
||||
mock_fas.get_group_member.return_value = ["user1", "user2"]
|
||||
self.sync_object.pagure.add_member_to_group.side_effect = PagureError(
|
||||
"PagureError"
|
||||
)
|
||||
|
||||
# Test
|
||||
self.sync_object.sync_group(fas_group, pagure_group)
|
||||
|
||||
# Assertions
|
||||
mock_fas.get_group_member.assert_called_with(fas_group)
|
||||
self.sync_object.pagure.get_group_members.assert_called_with(pagure_group)
|
||||
|
||||
self.sync_object.pagure.add_member_to_group.assert_called_with(
|
||||
"user2", pagure_group
|
||||
)
|
||||
self.sync_object.pagure.remove_member_from_group.assert_not_called()
|
||||
assert caplog.records[-1].message == "PagureError"
|
||||
|
||||
@patch("toddlers.plugins.pagure_fas_groups_sync.fedora_account")
|
||||
def test_sync_group_remove_member(self, mock_fas):
|
||||
"""Assert that removing member from group works as intended."""
|
||||
# Preparation
|
||||
fas_group = "fas_group"
|
||||
pagure_group = "pagure_group"
|
||||
self.sync_object.pagure.get_group_members.return_value = ["user1", "user2"]
|
||||
mock_fas.get_group_member.return_value = ["user1"]
|
||||
|
||||
# Test
|
||||
self.sync_object.sync_group(fas_group, pagure_group)
|
||||
|
||||
# Assertions
|
||||
mock_fas.get_group_member.assert_called_with(fas_group)
|
||||
self.sync_object.pagure.get_group_members.assert_called_with(pagure_group)
|
||||
|
||||
self.sync_object.pagure.remove_member_from_group.assert_called_with(
|
||||
"user2", pagure_group
|
||||
)
|
||||
self.sync_object.pagure.add_member_to_group.assert_not_called()
|
||||
|
||||
@patch("toddlers.plugins.pagure_fas_groups_sync.fedora_account")
|
||||
def test_sync_group_remove_member_user_not_in_pagure(self, mock_fas):
|
||||
"""
|
||||
Assert that removing member from group works
|
||||
as intended when user doesn't exist in pagure.
|
||||
"""
|
||||
# Preparation
|
||||
fas_group = "fas_group"
|
||||
pagure_group = "pagure_group"
|
||||
self.sync_object.pagure.get_group_members.return_value = ["user1", "user2"]
|
||||
self.sync_object.pagure.user_exists.return_value = False
|
||||
mock_fas.get_group_member.return_value = ["user1"]
|
||||
|
||||
# Test
|
||||
self.sync_object.sync_group(fas_group, pagure_group)
|
||||
|
||||
# Assertions
|
||||
mock_fas.get_group_member.assert_called_with(fas_group)
|
||||
self.sync_object.pagure.get_group_members.assert_called_with(pagure_group)
|
||||
|
||||
self.sync_object.pagure.remove_member_from_group.assert_not_called()
|
||||
self.sync_object.pagure.add_member_to_group.assert_not_called()
|
||||
|
||||
@patch("toddlers.plugins.pagure_fas_groups_sync.fedora_account")
|
||||
def test_sync_group_remove_member_failure(self, mock_fas, caplog):
|
||||
"""Assert that removing member from group works as intended."""
|
||||
# Preparation
|
||||
caplog.set_level(logging.ERROR)
|
||||
fas_group = "fas_group"
|
||||
pagure_group = "pagure_group"
|
||||
self.sync_object.pagure.get_group_members.return_value = ["user1", "user2"]
|
||||
mock_fas.get_group_member.return_value = ["user1"]
|
||||
self.sync_object.pagure.remove_member_from_group.side_effect = PagureError(
|
||||
"PagureError"
|
||||
)
|
||||
|
||||
# Test
|
||||
self.sync_object.sync_group(fas_group, pagure_group)
|
||||
|
||||
# Assertions
|
||||
mock_fas.get_group_member.assert_called_with(fas_group)
|
||||
self.sync_object.pagure.get_group_members.assert_called_with(pagure_group)
|
||||
|
||||
self.sync_object.pagure.remove_member_from_group.assert_called_with(
|
||||
"user2", pagure_group
|
||||
)
|
||||
self.sync_object.pagure.add_member_to_group.assert_not_called()
|
||||
assert caplog.records[-1].message == "PagureError"
|
||||
|
||||
@patch("toddlers.plugins.pagure_fas_groups_sync.fedora_account")
|
||||
@pytest.mark.parametrize(
|
||||
"mock_fas_group, mock_pagure_group", [([], ["user"]), (["user"], [])]
|
||||
)
|
||||
def test_sync_group_group_empty(self, mock_fas, mock_fas_group, mock_pagure_group):
|
||||
"""
|
||||
Assert that nothing is done if any of the retrieved groups is empty.
|
||||
That should not happen, there always needs to be at least one member
|
||||
in the group.
|
||||
"""
|
||||
# Preparation
|
||||
fas_group = "fas_group"
|
||||
pagure_group = "pagure_group"
|
||||
self.sync_object.pagure.get_group_members.return_value = mock_pagure_group
|
||||
mock_fas.get_group_member.return_value = mock_fas_group
|
||||
|
||||
# Test
|
||||
self.sync_object.sync_group(fas_group, pagure_group)
|
||||
|
||||
# Assertions
|
||||
mock_fas.get_group_member.assert_called_with(fas_group)
|
||||
self.sync_object.pagure.get_group_members.assert_called_with(pagure_group)
|
||||
|
||||
self.sync_object.pagure.add_member_to_group.assert_not_called()
|
||||
self.sync_object.pagure.remove_member_from_group.assert_not_called()
|
||||
|
||||
|
||||
class TestMain:
|
||||
"""
|
||||
Test class for `toddlers.plugins.pagure_fas_groups_sync.main` function.
|
||||
"""
|
||||
|
||||
def test_main_no_args(self, capsys):
|
||||
"""Assert that help is printed if no arg is provided."""
|
||||
# Test
|
||||
with pytest.raises(SystemExit):
|
||||
pagure_fas_groups_sync.main([])
|
||||
|
||||
# Assertions
|
||||
out, err = capsys.readouterr()
|
||||
assert out == ""
|
||||
# Expecting something along these lines, but don't make the test too tight:
|
||||
#
|
||||
# usage: pytest [-h] [--dry-run] [-q | --debug] conf [username]
|
||||
# pytest: error: the following arguments are required: conf
|
||||
assert err.startswith("usage:")
|
||||
assert "error: the following arguments are required:" in err
|
||||
|
||||
@patch("toddlers.utils.pagure.set_pagure")
|
||||
@patch("toddlers.utils.fedora_account.set_fasjson")
|
||||
@patch("toddlers.plugins.pagure_fas_groups_sync.PagureFASGroupsSync.sync_group")
|
||||
def test_main(self, mock_sync_group, mock_fas, mock_pagure):
|
||||
"""Assert that main is processed correctly."""
|
||||
# Test
|
||||
pagure_fas_groups_sync.main(
|
||||
[
|
||||
"--api-key",
|
||||
"api_key",
|
||||
"--group",
|
||||
"fas_group",
|
||||
"--target-group",
|
||||
"pagure_group",
|
||||
"--debug",
|
||||
]
|
||||
)
|
||||
|
||||
# Assertions
|
||||
mock_pagure.assert_called_with(
|
||||
{"pagure_url": "https://pagure.io", "pagure_api_key": "api_key"}
|
||||
)
|
||||
mock_fas.assert_called_with({"fas_url": "https://fasjson.fedoraproject.org"})
|
||||
mock_sync_group.assert_called_with("fas_group", "pagure_group")
|
|
@ -3,7 +3,7 @@ Unit tests for `toddlers.utils.pagure`.
|
|||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import call, Mock, patch
|
||||
from unittest.mock import call, MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
@ -1455,3 +1455,216 @@ class TestPagureAssignMaintainerToProject:
|
|||
data=json.dumps(payload),
|
||||
headers=self.pagure.get_auth_header(),
|
||||
)
|
||||
|
||||
|
||||
class TestPagureAddMemberToGroup:
|
||||
"""
|
||||
Test class for `toddlers.pagure.Pagure.add_member_to_group` method.
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
"""
|
||||
Setup method for the test class.
|
||||
"""
|
||||
config = {
|
||||
"pagure_url": "https://pagure.io",
|
||||
"pagure_api_key": "Very secret key",
|
||||
}
|
||||
self.pagure = pagure.set_pagure(config)
|
||||
self.pagure._requests_session = Mock()
|
||||
|
||||
def test_add_member_to_group(self):
|
||||
"""
|
||||
Assert that adding member to group is processed correctly.
|
||||
"""
|
||||
response_mock = Mock()
|
||||
|
||||
self.pagure._requests_session.post.return_value = response_mock
|
||||
|
||||
user = "conrad_kurze"
|
||||
group = "adeptus_astartes"
|
||||
payload = {
|
||||
"user": user,
|
||||
}
|
||||
|
||||
self.pagure.add_member_to_group(user, group)
|
||||
|
||||
self.pagure._requests_session.post.assert_called_with(
|
||||
"https://pagure.io/api/0/group/" + group + "/add",
|
||||
data=json.dumps(payload),
|
||||
headers=self.pagure.get_auth_header(),
|
||||
)
|
||||
|
||||
def test_add_member_to_group_failure(self):
|
||||
"""
|
||||
Assert that failing to add member to group is handled correctly.
|
||||
"""
|
||||
response_mock = MagicMock()
|
||||
response_mock.ok = False
|
||||
|
||||
self.pagure._requests_session.post.return_value = response_mock
|
||||
|
||||
user = "conrad_kurze"
|
||||
group = "adeptus_astartes"
|
||||
payload = {
|
||||
"user": user,
|
||||
}
|
||||
|
||||
expected_error = "Couldn't add user '{0}' to group '{1}'".format(user, group)
|
||||
|
||||
with pytest.raises(PagureError, match=expected_error):
|
||||
self.pagure.add_member_to_group(user, group)
|
||||
|
||||
self.pagure._requests_session.post.assert_called_with(
|
||||
"https://pagure.io/api/0/group/" + group + "/add",
|
||||
data=json.dumps(payload),
|
||||
headers=self.pagure.get_auth_header(),
|
||||
)
|
||||
|
||||
|
||||
class TestPagureRemoveMemberFromGroup:
|
||||
"""
|
||||
Test class for `toddlers.pagure.Pagure.remove_member_from_group` method.
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
"""
|
||||
Setup method for the test class.
|
||||
"""
|
||||
config = {
|
||||
"pagure_url": "https://pagure.io",
|
||||
"pagure_api_key": "Very secret key",
|
||||
}
|
||||
self.pagure = pagure.set_pagure(config)
|
||||
self.pagure._requests_session = Mock()
|
||||
|
||||
def test_remove_member_from_group(self):
|
||||
"""
|
||||
Assert that removing member from group is processed correctly.
|
||||
"""
|
||||
response_mock = Mock()
|
||||
|
||||
self.pagure._requests_session.post.return_value = response_mock
|
||||
|
||||
user = "conrad_kurze"
|
||||
group = "adeptus_astartes"
|
||||
payload = {
|
||||
"user": user,
|
||||
}
|
||||
|
||||
self.pagure.remove_member_from_group(user, group)
|
||||
|
||||
self.pagure._requests_session.post.assert_called_with(
|
||||
"https://pagure.io/api/0/group/" + group + "/remove",
|
||||
data=json.dumps(payload),
|
||||
headers=self.pagure.get_auth_header(),
|
||||
)
|
||||
|
||||
def test_remove_member_from_group_failure(self):
|
||||
"""
|
||||
Assert that failing to remove member from group is handled correctly.
|
||||
"""
|
||||
response_mock = MagicMock()
|
||||
response_mock.ok = False
|
||||
|
||||
self.pagure._requests_session.post.return_value = response_mock
|
||||
|
||||
user = "conrad_kurze"
|
||||
group = "adeptus_astartes"
|
||||
payload = {
|
||||
"user": user,
|
||||
}
|
||||
|
||||
expected_error = "Couldn't remove user '{0}' from group '{1}'".format(
|
||||
user, group
|
||||
)
|
||||
|
||||
with pytest.raises(PagureError, match=expected_error):
|
||||
self.pagure.remove_member_from_group(user, group)
|
||||
|
||||
self.pagure._requests_session.post.assert_called_with(
|
||||
"https://pagure.io/api/0/group/" + group + "/remove",
|
||||
data=json.dumps(payload),
|
||||
headers=self.pagure.get_auth_header(),
|
||||
)
|
||||
|
||||
|
||||
class TestPagureGetGroupMembers:
|
||||
"""
|
||||
Test class for `toddlers.pagure.Pagure.get_group_members` method.
|
||||
"""
|
||||
|
||||
def setup_method(self):
|
||||
"""
|
||||
Setup method for the test class.
|
||||
"""
|
||||
config = {
|
||||
"pagure_url": "https://pagure.io",
|
||||
"pagure_api_key": "Very secret key",
|
||||
}
|
||||
self.pagure = pagure.set_pagure(config)
|
||||
self.pagure._requests_session = Mock()
|
||||
|
||||
def test_get_group_members(self):
|
||||
"""
|
||||
Assert that retrieving members in group is processed correctly.
|
||||
"""
|
||||
response_mock = Mock()
|
||||
user = "conrad_kurze"
|
||||
data = {"members": [user]}
|
||||
response_mock.json.return_value = data
|
||||
|
||||
self.pagure._requests_session.get.return_value = response_mock
|
||||
|
||||
group = "adeptus_astartes"
|
||||
|
||||
result = self.pagure.get_group_members(group)
|
||||
|
||||
assert result == [user]
|
||||
|
||||
self.pagure._requests_session.get.assert_called_with(
|
||||
"https://pagure.io/api/0/group/" + group,
|
||||
headers=self.pagure.get_auth_header(),
|
||||
)
|
||||
|
||||
def test_get_group_members_no_group(self):
|
||||
"""
|
||||
Assert that when group doesn't exist is handled correctly.
|
||||
"""
|
||||
response_mock = MagicMock()
|
||||
response_mock.ok = False
|
||||
response_mock.status_code = 404
|
||||
|
||||
self.pagure._requests_session.get.return_value = response_mock
|
||||
|
||||
group = "adeptus_astartes"
|
||||
|
||||
result = self.pagure.get_group_members(group)
|
||||
|
||||
assert not result
|
||||
|
||||
self.pagure._requests_session.get.assert_called_with(
|
||||
"https://pagure.io/api/0/group/" + group,
|
||||
headers=self.pagure.get_auth_header(),
|
||||
)
|
||||
|
||||
def test_get_group_members_failure(self):
|
||||
"""
|
||||
Assert that failing to obtain members of group is handled correctly.
|
||||
"""
|
||||
response_mock = MagicMock()
|
||||
response_mock.ok = False
|
||||
|
||||
self.pagure._requests_session.get.return_value = response_mock
|
||||
|
||||
group = "adeptus_astartes"
|
||||
|
||||
expected_error = "Couldn't get members of group '{0}'".format(group)
|
||||
|
||||
with pytest.raises(PagureError, match=expected_error):
|
||||
self.pagure.get_group_members(group)
|
||||
|
||||
self.pagure._requests_session.get.assert_called_with(
|
||||
"https://pagure.io/api/0/group/" + group,
|
||||
headers=self.pagure.get_auth_header(),
|
||||
)
|
||||
|
|
|
@ -295,6 +295,18 @@ bug_fixes = '2022-11-26'
|
|||
security_fixes = '2022-03-08'
|
||||
bug_fixes = '2022-03-08'
|
||||
|
||||
[consumer_config.pagure_fas_groups_sync]
|
||||
pagure_url = "https://pagure.io"
|
||||
# Token needs to have following permissions:
|
||||
# - adding_member_to_group
|
||||
# - removing_member_from_group
|
||||
pagure_api_key = "API token for pagure"
|
||||
|
||||
[consumer_config.pagure_fas_groups_sync.group_map]
|
||||
#Mapping of FAS groups to Pagure groups
|
||||
infra-sig = 'fedora-infra'
|
||||
|
||||
|
||||
[qos]
|
||||
prefetch_size = 0
|
||||
prefetch_count = 25
|
||||
|
|
205
toddlers/plugins/pagure_fas_groups_sync.py
Normal file
205
toddlers/plugins/pagure_fas_groups_sync.py
Normal file
|
@ -0,0 +1,205 @@
|
|||
"""
|
||||
This toddler synchronizes groups from FAS to pagure.io.
|
||||
|
||||
Authors: Michal Konecny <mkonecny@redhat.com>
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from typing import Dict
|
||||
|
||||
from fedora_messaging.api import Message
|
||||
from noggin_messages import MemberSponsorV1
|
||||
|
||||
from toddlers.base import ToddlerBase
|
||||
from toddlers.exceptions import PagureError
|
||||
from toddlers.utils import fedora_account, pagure
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PagureFASGroupsSync(ToddlerBase):
|
||||
"""
|
||||
Synchronize FAS groups with Pagure.
|
||||
|
||||
Consumes group membership change messages and process them
|
||||
and consumes trigger to sync all the configured groups.
|
||||
"""
|
||||
|
||||
name: str = "pagure_fas_groups_sync"
|
||||
|
||||
amqp_topics: list = [
|
||||
"org.fedoraproject.*.fas.group.member.sponsor",
|
||||
"org.fedoraproject.*.toddlers.trigger.pagure_fas_groups_sync",
|
||||
]
|
||||
|
||||
# Pagure object
|
||||
pagure: pagure.Pagure
|
||||
|
||||
# Group mapping from FAS to pagure
|
||||
group_map: Dict[str, str]
|
||||
|
||||
def accepts_topic(self, topic: str) -> bool:
|
||||
"""
|
||||
Return a boolean if this toddler consumes messages from topic.
|
||||
|
||||
:arg topic: Topic to check.
|
||||
|
||||
:returns: True if topic is accepted, False otherwise.
|
||||
"""
|
||||
if topic.startswith("org.fedoraproject."):
|
||||
if topic.endswith("fas.group.member.sponsor"):
|
||||
return True
|
||||
if topic.endswith("toddlers.trigger.pagure_fas_groups_sync"):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def process(
|
||||
self,
|
||||
config: dict,
|
||||
message: Message,
|
||||
) -> None:
|
||||
"""Process a given message.
|
||||
|
||||
:arg config: Toddlers configuration
|
||||
:arg message: Message to process
|
||||
"""
|
||||
if _log.isEnabledFor(logging.DEBUG):
|
||||
_log.debug("Processing message:\n%s", json.dumps(message.body, indent=2))
|
||||
topic = message.topic
|
||||
|
||||
self.pagure = pagure.set_pagure(config)
|
||||
fedora_account.set_fasjson(config)
|
||||
|
||||
group_map = config.get("group_map", {})
|
||||
|
||||
if topic.endswith("toddlers.trigger.pagure_fas_groups_sync"):
|
||||
for group in group_map:
|
||||
self.sync_group(group, group_map[group])
|
||||
|
||||
if topic.endswith("fas.group.member.sponsor"):
|
||||
member_sponsor_message = MemberSponsorV1(message.body)
|
||||
user = member_sponsor_message.user_name
|
||||
for group in member_sponsor_message.groups:
|
||||
if group in group_map:
|
||||
try:
|
||||
if self.pagure.user_exists(user):
|
||||
self.pagure.add_member_to_group(user, group_map[group])
|
||||
except PagureError as e:
|
||||
_log.exception(str(e))
|
||||
else:
|
||||
_log.info(
|
||||
"User '%s' was added to group '%s', "
|
||||
"but we don't care about this group. "
|
||||
"Skipping.",
|
||||
user,
|
||||
group,
|
||||
)
|
||||
|
||||
def sync_group(self, fas_group: str, pagure_group: str):
|
||||
"""
|
||||
Synchronize FAS group with Pagure group.
|
||||
|
||||
Compares FAS group and Pagure group and modifies the Pagure
|
||||
group to correspond with the specified FAS group.
|
||||
|
||||
:arg fas_group: FAS group to check
|
||||
:arg pagure_group: Pagure group to sync
|
||||
"""
|
||||
_log.info(
|
||||
"Syncing FAS group '%s' with pagure group '%s'", fas_group, pagure_group
|
||||
)
|
||||
group_members_fas = fedora_account.get_group_member(fas_group)
|
||||
_log.debug("FAS group members: [%s]", group_members_fas)
|
||||
group_members_pagure = self.pagure.get_group_members(pagure_group)
|
||||
_log.debug("Pagure group members: [%s]", group_members_pagure)
|
||||
|
||||
if not group_members_fas or not group_members_pagure:
|
||||
_log.warning(
|
||||
"FAS group or Pagure group is empty. This shouldn't happen. Skipping sync."
|
||||
)
|
||||
return
|
||||
add_members = [
|
||||
user for user in group_members_fas if user not in group_members_pagure
|
||||
]
|
||||
remove_members = [
|
||||
user for user in group_members_pagure if user not in group_members_fas
|
||||
]
|
||||
|
||||
for user in add_members:
|
||||
try:
|
||||
if self.pagure.user_exists(user):
|
||||
self.pagure.add_member_to_group(user, pagure_group)
|
||||
except PagureError as e:
|
||||
_log.exception(str(e))
|
||||
|
||||
for user in remove_members:
|
||||
try:
|
||||
if self.pagure.user_exists(user):
|
||||
self.pagure.remove_member_from_group(user, pagure_group)
|
||||
except PagureError as e:
|
||||
_log.exception(str(e))
|
||||
|
||||
_log.info("Sync complete")
|
||||
|
||||
|
||||
def main(args):
|
||||
"""Run toddler class without trigger message."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Sync the group membership from FAS to pagure."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fas-url",
|
||||
default="https://fasjson.fedoraproject.org",
|
||||
help="URL to the FAS instance to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pagure-url",
|
||||
default="https://pagure.io",
|
||||
help="URL to the Pagure instance to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-key",
|
||||
help="Pagure API key to use.",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--group",
|
||||
help="Sync a specific group from FAS to pagure.",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-group",
|
||||
help="Name of the group on the pagure side that the group specified "
|
||||
"in --group should be synced to.",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Print the debugging output",
|
||||
)
|
||||
args = parser.parse_args(args)
|
||||
_log.addHandler(logging.StreamHandler(sys.stdout))
|
||||
if args.debug:
|
||||
_log.setLevel(logging.DEBUG)
|
||||
|
||||
pagure_fas_sync_toddler = PagureFASGroupsSync()
|
||||
pagure_fas_sync_toddler.pagure = pagure.set_pagure(
|
||||
{"pagure_url": args.pagure_url, "pagure_api_key": args.api_key}
|
||||
)
|
||||
fedora_account.set_fasjson({"fas_url": args.fas_url})
|
||||
pagure_fas_sync_toddler.sync_group(args.group, args.target_group)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
try:
|
||||
main(sys.argv[1:])
|
||||
except KeyboardInterrupt:
|
||||
pass
|
|
@ -18,7 +18,7 @@ Examples:
|
|||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from toddlers.exceptions import PagureError
|
||||
from toddlers.utils import requests
|
||||
|
@ -936,3 +936,169 @@ class Pagure:
|
|||
namespace, repo
|
||||
)
|
||||
)
|
||||
|
||||
def add_member_to_group(self, user: str, group: str) -> None:
|
||||
"""
|
||||
Add member to pagure group.
|
||||
|
||||
Params:
|
||||
user: User to add
|
||||
group: Group to add user to
|
||||
|
||||
Raises:
|
||||
`toddlers.utils.exceptions.PagureError``: When user can't be added to group.
|
||||
"""
|
||||
api_url = "{0}/api/0/group/{1}/add".format(self._pagure_url, group)
|
||||
|
||||
payload = {"user": user}
|
||||
headers = self.get_auth_header()
|
||||
|
||||
log.debug("Adding user '%s' to group '%s'", user, group)
|
||||
response = self._requests_session.post(
|
||||
api_url, data=json.dumps(payload), headers=headers
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
log.error(
|
||||
"Error when adding user '%s' to group '%s'. " "Got status_code '%s'.",
|
||||
user,
|
||||
group,
|
||||
response.status_code,
|
||||
)
|
||||
|
||||
response_json = None
|
||||
if response.headers.get("content-type") == "application/json":
|
||||
response_json = response.json()
|
||||
log.error("Received response: %s", response.json())
|
||||
|
||||
raise PagureError(
|
||||
(
|
||||
"Couldn't add user '{0}' to group '{1}'\n\n"
|
||||
"Request to '{2}':\n\n"
|
||||
"Response:\n"
|
||||
"{3}\n\n"
|
||||
"Status code: {4}"
|
||||
).format(
|
||||
user,
|
||||
group,
|
||||
api_url,
|
||||
response_json,
|
||||
response.status_code,
|
||||
)
|
||||
)
|
||||
|
||||
log.debug("User '%s' added to group '%s'", user, group)
|
||||
|
||||
def remove_member_from_group(self, user: str, group: str) -> None:
|
||||
"""
|
||||
Remove member from pagure group.
|
||||
|
||||
Params:
|
||||
user: User to remove
|
||||
group: Group to remove user from
|
||||
|
||||
Raises:
|
||||
`toddlers.utils.exceptions.PagureError``: When user can't be removed to group.
|
||||
"""
|
||||
api_url = "{0}/api/0/group/{1}/remove".format(self._pagure_url, group)
|
||||
payload = {"user": user}
|
||||
headers = self.get_auth_header()
|
||||
|
||||
log.debug("Removing user '%s' from group '%s'", user, group)
|
||||
response = self._requests_session.post(
|
||||
api_url, data=json.dumps(payload), headers=headers
|
||||
)
|
||||
|
||||
if not response.ok:
|
||||
log.error(
|
||||
"Error when removing user '%s' from group '%s'. "
|
||||
"Got status_code '%s'.",
|
||||
user,
|
||||
group,
|
||||
response.status_code,
|
||||
)
|
||||
|
||||
response_json = None
|
||||
if response.headers.get("content-type") == "application/json":
|
||||
response_json = response.json()
|
||||
log.error("Received response: %s", response.json())
|
||||
|
||||
raise PagureError(
|
||||
(
|
||||
"Couldn't remove user '{0}' from group '{1}'\n\n"
|
||||
"Request to '{2}':\n\n"
|
||||
"Response:\n"
|
||||
"{3}\n\n"
|
||||
"Status code: {4}"
|
||||
).format(
|
||||
user,
|
||||
group,
|
||||
api_url,
|
||||
response_json,
|
||||
response.status_code,
|
||||
)
|
||||
)
|
||||
|
||||
log.debug("User '%s' removed from group '%s'", user, group)
|
||||
|
||||
def get_group_members(self, group: str) -> List[str]:
|
||||
"""
|
||||
Get members of group.
|
||||
|
||||
Params:
|
||||
group: Group name
|
||||
|
||||
Returns:
|
||||
List of users or empty list if group doesn't exists..
|
||||
|
||||
Raises:
|
||||
`toddlers.utils.exceptions.PagureError``: When getting members fail.
|
||||
"""
|
||||
result = []
|
||||
api_url = "{0}/api/0/group/{1}".format(self._pagure_url, group)
|
||||
headers = self.get_auth_header()
|
||||
|
||||
log.debug("Getting members of group '%s'", group)
|
||||
response = self._requests_session.get(api_url, headers=headers)
|
||||
|
||||
if response.ok:
|
||||
if "members" in response.json():
|
||||
result = response.json()["members"]
|
||||
else:
|
||||
raise PagureError(
|
||||
"The 'members' parameter is missing in response. "
|
||||
"JSON response: {0}".format(response.json())
|
||||
)
|
||||
elif response.status_code == 404:
|
||||
return result
|
||||
else:
|
||||
log.error(
|
||||
"Error when retrieving members from group '%s'. "
|
||||
"Got status_code '%s'.",
|
||||
group,
|
||||
response.status_code,
|
||||
)
|
||||
|
||||
response_json = None
|
||||
if response.headers.get("content-type") == "application/json":
|
||||
response_json = response.json()
|
||||
log.error("Received response: %s", response.json())
|
||||
|
||||
raise PagureError(
|
||||
(
|
||||
"Couldn't get members of group '{0}'\n\n"
|
||||
"Request to '{1}:\n\n"
|
||||
"Response:\n"
|
||||
"{2}\n\n"
|
||||
"Status code: {3}"
|
||||
).format(
|
||||
group,
|
||||
api_url,
|
||||
response_json,
|
||||
response.status_code,
|
||||
)
|
||||
)
|
||||
|
||||
log.debug("Members retrieved for group '%s'", group)
|
||||
|
||||
return result
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue