#369 Update production branch
Closed 5 months ago by lenkaseg. Opened 5 months ago by lenkaseg.

file modified
+4 -4
@@ -1,4 +1,4 @@ 

- # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.

+ # This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.

  

  [[package]]

  name = "arrow"
@@ -1021,13 +1021,13 @@ 

  

  [[package]]

  name = "journal-to-fedora-messaging-messages"

- version = "1.0.1"

+ version = "1.0.3"

  description = "A schema package for messages sent by Journal to Fedora Messaging"

  optional = false

  python-versions = "<4.0,>=3.9"

  files = [

-     {file = "journal_to_fedora_messaging_messages-1.0.1-py3-none-any.whl", hash = "sha256:6eb7b01054b89bd304d16105066e47a15f6627ef022c56f4c3d05e48519a7ecf"},

-     {file = "journal_to_fedora_messaging_messages-1.0.1.tar.gz", hash = "sha256:e9cc8295095fadf19285bb0b9439dca2db06dda2bf64fc96f521fa52f85c9d57"},

+     {file = "journal_to_fedora_messaging_messages-1.0.3-py3-none-any.whl", hash = "sha256:51f45f19b5b1d4fa9d7aa547321f559f66faee56e81f18ee37f3366ed1d747a8"},

+     {file = "journal_to_fedora_messaging_messages-1.0.3.tar.gz", hash = "sha256:6efd86b33d11dbdcee6374483c44590b122fd93a094e38271075ff51fa6d39a2"},

  ]

  

  [package.dependencies]

file modified
+17 -5
@@ -1,7 +1,7 @@ 

  import json

  import os

  import sys

- from unittest.mock import MagicMock

+ from unittest.mock import MagicMock, patch

  

  from fedora_messaging.config import conf as fm_config

  import pytest
@@ -46,10 +46,22 @@ 

          },

      )

  

-     toddler_obj = toddler_cls()

-     # disable the cache

-     cache.configure(backend="dogpile.cache.null", replace_existing_backend=True)

-     return toddler_obj

+     ipa_patcher = None

+     if toddler_cls.__name__ == "CleanPackagerGroups":

+         ipa_patcher = patch("toddlers.plugins.cleaning_packager_groups.ipalib.api")

+         mock_api = ipa_patcher.start()

+         mock_api.bootstrap.return_value = None

+         mock_api.load_plugins.return_value = None

+         mock_api.finalize.return_value = None

+ 

+     try:

+         toddler_obj = toddler_cls()

+         # disable the cache

+         cache.configure(backend="dogpile.cache.null", replace_existing_backend=True)

+         return toddler_obj

+     finally:

+         if ipa_patcher:

+             ipa_patcher.stop()

  

  

  @pytest.fixture

@@ -5,6 +5,7 @@ 

  import logging

  from unittest.mock import MagicMock, patch

  

+ from fedora_messaging.message import Message

  from journal_to_fedora_messaging_messages.ipa import IpaGroupRemoveMemberV1

  import pytest

  
@@ -36,6 +37,11 @@ 

      )

  

  

+ @pytest.fixture

+ def topic_message():

+     return Message

+ 

+ 

  class TestAcceptTopic:

      """

      Test class for `toddler.plugins.cleaning_packager_groups.CleanPackagerGroups.accepts_topic`
@@ -56,6 +62,7 @@ 

              "org.fedoraproject.*.ipa.group_remove_member.v1",

              "org.fedoraproject.prod.ipa.group_remove_member.v1",

              "org.fedoraproject.stg.ipa.group_remove_member.v1",

+             "org.fedoraproject.*.toddlers.trigger.clean_packagers_groups",

          ],

      )

      def test_accepts_topic_valid(self, topic, toddler):
@@ -72,9 +79,20 @@ 

  

      def setup_method(self):

          """Initialize toddler."""

+         self._ipa_patcher = patch(

+             "toddlers.plugins.cleaning_packager_groups.ipalib.api"

+         )

+         mock_api = self._ipa_patcher.start()

+         mock_api.bootstrap.return_value = None

+         mock_api.load_plugins.return_value = None

+         mock_api.finalize.return_value = None

+ 

          self.toddler_cls = cleaning_packager_groups.CleanPackagerGroups()

          self.toddler_cls._ipa_session = MagicMock()

  

+     def teardown_method(self):

+         self._ipa_patcher.stop()

+ 

      @patch("toddlers.utils.pagure.set_pagure")

      def test_not_packager_group(

          self, mock_set_pagure, group_remove_member_message, caplog
@@ -112,11 +130,15 @@ 

                      "Distgit groups found: ['group1', 'group2']",

                      "User lenkaseg was removed from packager group, removing from "

                      "packager-related groups as well.",

+                     "Removing user lenkaseg from distgit group packager",

                      "User lenkaseg removed from distgit group: packager",

                      "Fetching groups user lenkaseg is member of in ipa:",

                      "Ipa groups: ['group1', 'group3']",

                      "User lenkaseg should be removed from following groups: ['group1']",

-                     "User lenkaseg removed from ipa group: group1",

+                     "User lenkaseg is not a sponsor of ipa group group1, skipping sponsor removal",

+                     "Removing user lenkaseg from members of ipa group group1",

+                     "User lenkaseg removed from members of ipa group group1",

+                     "Removing user lenkaseg from distgit group group1",

                      "User lenkaseg removed from distgit group: group1",

                  ),

              ),
@@ -137,6 +159,7 @@ 

                      "Distgit groups found: ['group1', 'group2']",

                      "User lenkaseg was removed from packager group, removing from "

                      "packager-related groups as well.",

+                     "Removing user lenkaseg from distgit group packager",

                      "User lenkaseg removed from distgit group: packager",

                      "Fetching groups user lenkaseg is member of in ipa:",

                      "Ipa groups: ['group3', 'group4']",
@@ -152,6 +175,7 @@ 

                      "Distgit groups found: ['group1', 'group2']",

                      "User lenkaseg was removed from packager group, removing from "

                      "packager-related groups as well.",

+                     "Removing user lenkaseg from distgit group packager",

                      "User lenkaseg removed from distgit group: packager",

                      "Fetching groups user lenkaseg is member of in ipa:",

                      "Ipa groups: []",
@@ -186,6 +210,23 @@ 

          mock_pagure.return_value = mock_pagure_obj

  

          self.toddler_cls._ipa_session.Command.user_show.return_value = ipa_groups

+ 

+         # Mock group_show for the first test case where IPA removal should happen

+         if distgit_groups == ["group1", "group2"] and ipa_groups == {

+             "result": {"memberof_group": ["group1", "group3"]}

+         }:

+             self.toddler_cls._ipa_session.Command.group_show.return_value = {

+                 "result": {"member_user": ["lenkaseg"], "membermanager_user": []}

+             }

+             self.toddler_cls._ipa_session.Command.group_remove_member.return_value = {

+                 "completed": 1

+             }

+         else:

+             # For other test cases, user is not in IPA group

+             self.toddler_cls._ipa_session.Command.group_show.return_value = {

+                 "result": {"member_user": [], "membermanager_user": []}

+             }

+ 

          caplog.set_level(logging.INFO)

          config = {

              "pagure_url": "https://example.io",
@@ -198,19 +239,23 @@ 

          assert recorded_messages == messages

  

      @pytest.mark.parametrize(

-         "ipa_remove_member,error,message_1,message_2",

+         "ipa_remove_member,error,messages",

          (

              (

                  Exception,

                  None,

-                 "User lenkaseg removed from distgit group: group1",

-                 "Error while removing user lenkaseg from ipa group group1",

+                 (

+                     "User lenkaseg removed from distgit group: group1",

+                     "Error while removing user lenkaseg from ipa group group1",

+                 ),

              ),

              (

                  None,

                  PagureError,

-                 "Error while removing user lenkaseg from distgit group group1",

-                 "User lenkaseg removed from ipa group: group1",

+                 (

+                     "Error while removing user lenkaseg from distgit group group1",

+                     "User lenkaseg removed from members of ipa group group1",

+                 ),

              ),

          ),

          ids=("ipa_error", "distgit_error"),
@@ -221,8 +266,7 @@ 

          mock_set_pagure,

          ipa_remove_member,

          error,

-         message_1,

-         message_2,

+         messages,

          group_remove_member_message,

          caplog,

      ):
@@ -236,9 +280,20 @@ 

              "result": {"memberof_group": ["group1", "group3"]}

          }

  

-         self.toddler_cls._ipa_session.Command.group_remove_member.side_effect = (

-             ipa_remove_member

-         )

+         self.toddler_cls._ipa_session.Command.group_show.return_value = {

+             "result": {"member_user": ["lenkaseg"], "membermanager_user": []}

+         }

+ 

+         if ipa_remove_member:

+             self.toddler_cls._ipa_session.Command.group_remove_member.side_effect = (

+                 ipa_remove_member

+             )

+         else:

+             # Return successful result when no IPA error is expected

+             self.toddler_cls._ipa_session.Command.group_remove_member.return_value = {

+                 "completed": 1

+             }

+             self.toddler_cls._ipa_session.Command.group_remove_member.side_effect = None

  

          caplog.set_level(logging.INFO)

          config = {
@@ -252,5 +307,419 @@ 

          self.toddler_cls._ipa_session.Command.user_show.assert_called_with(

              uid="lenkaseg"

          )

-         assert caplog.records[-1].message == message_1

-         assert caplog.records[-2].message == message_2

+         for message in messages:

+             assert message in [r.message for r in caplog.records]

+ 

+     @pytest.mark.parametrize(

+         "ipa_output,expected_logs",

+         [

+             (

+                 {"completed": 1},

+                 [

+                     "User lenkaseg removed from members of ipa group group1",

+                     "User lenkaseg removed from distgit group: group1",

+                 ],

+             ),

+             (

+                 {"completed": 0},

+                 [

+                     "Removing user lenkaseg from members of ipa group group1 was not "

+                     "successful, output:",

+                     "User lenkaseg removed from distgit group: group1",

+                 ],

+             ),

+         ],

+         ids=[

+             "user_removal_successful",

+             "user_removal_failed",

+         ],

+     )

+     def test_process_removal_ipa_output_scenarios(

+         self, ipa_output, expected_logs, caplog

+     ):

+         """

+         Assert correct behaviour of ipa_session.Command.group_remove_member() output.

+         """

+         self.toddler_cls._ipa_session.Command.group_remove_member.return_value = (

+             ipa_output

+         )

+ 

+         self.toddler_cls._ipa_session.Command.user_show.return_value = {

+             "result": {"memberof_group": ["group1", "group3"]}

+         }

+ 

+         self.toddler_cls._ipa_session.Command.group_show.return_value = {

+             "result": {"member_user": ["lenkaseg"], "membermanager_user": []}

+         }

+ 

+         self.toddler_cls.dist_git = MagicMock()

+         self.toddler_cls.dist_git.remove_member_from_group.return_value = None

+ 

+         config = {"watched_groups": ["packager"]}

+         user = "lenkaseg"

+         group = "packager"

+         distgit_groups = ["group1", "group2"]

+ 

+         caplog.set_level(logging.INFO)

+         self.toddler_cls._process_removal(config, user, group, distgit_groups)

+ 

+         # Verify expected log messages

+         for expected_log in expected_logs:

+             assert expected_log in caplog.text

+ 

+         # Verify IPA calls

+         self.toddler_cls._ipa_session.Command.group_remove_member.assert_called_once_with(

+             cn="group1", user=user

+         )

+ 

+         # Verify distgit removal was called (both initial and intersection)

+         self.toddler_cls.dist_git.remove_member_from_group.assert_any_call(user, group)

+         self.toddler_cls.dist_git.remove_member_from_group.assert_any_call(

+             user, "group1"

+         )

+ 

+     @patch(

+         "toddlers.plugins.cleaning_packager_groups.CleanPackagerGroups.find_and_remove"

+     )

+     def test_error_initializing_ipa_session(self, find_and_remove, caplog):

+         """

+         If an error occurs initializing IPA session, logs error and stops.

+         """

+         self.toddler_cls._ipa_session.__enter__.side_effect = Exception(

+             "IPA connection failed"

+         )

+         config = {

+             "watched_groups": ["packager"],

+             "dist_git_url": "http://example.com/pagure",

+             "dist_git_token": "dummy-token",

+         }

+         caplog.set_level(logging.INFO)

+ 

+         # This should raise the exception and not call find_and_remove

+         with pytest.raises(Exception, match="IPA connection failed"):

+             self.toddler_cls.process(

+                 config, Message(topic="toddlers.trigger.clean_packagers_groups")

+             )

+ 

+ 

+ class TestFindAndRemove:

+     """

+     Test class for

+     `toddler.plugins.cleaning_packager_groups.CleanPackagerGroups.find_and_remove`

+     method

+     """

+ 

+     def setup_method(self):

+         # Patch the IPA API to prevent bootstrap() already called error

+         self._ipa_patcher = patch(

+             "toddlers.plugins.cleaning_packager_groups.ipalib.api"

+         )

+         mock_api = self._ipa_patcher.start()

+         mock_api.bootstrap.return_value = None

+         mock_api.load_plugins.return_value = None

+         mock_api.finalize.return_value = None

+ 

+         self.toddler_cls = cleaning_packager_groups.CleanPackagerGroups()

+         self.mock_distgit = MagicMock()

+         self.mock_ipa_session = MagicMock()

+         self.toddler_cls.dist_git = self.mock_distgit

+         self.toddler_cls._ipa_session = self.mock_ipa_session

+ 

+     def teardown_method(self):

+         self._ipa_patcher.stop()

+ 

+     @patch("toddlers.utils.pagure.set_pagure")

+     def test_topic_playtime_call(self, mock_set_pagure, topic_message, caplog):

+         """

+         Assert that if the group in the message is not "packager", plugin stops.

+         """

+         self.toddler_cls.find_and_remove = MagicMock()

+         topic_message.topic = (

+             "org.fedoraproject.stg.toddlers.trigger.clean_packagers_groups"

+         )

+         caplog.set_level(logging.INFO)

+         config = {

+             "pagure_url": "https://pagure.io",

+             "pagure_stg": "https://stg.pagure.io",

+             "watched_groups": ["packager"],

+         }

+         self.toddler_cls.process(config, topic_message)

+         self.toddler_cls.find_and_remove.assert_called_once()

+ 

+     def test_all_members_are_packager_members(self, caplog):

+         """

+         If all distgit group members are IPA packager members, no removal occurs.

+         """

+         self.mock_distgit.get_all_groups.return_value = ["group1"]

+         self.mock_distgit.get_group_members.return_value = ["lenkaseg", "samyak"]

+         self.mock_ipa_session.Command.group_show.return_value = {

+             "result": {

+                 "member_user": ["lenkaseg", "samyak"],

+                 "membermanager_user": ["lenkaseg", "samyak"],

+             }

+         }

+         config = {"watched_groups": ["packager"]}

+         caplog.set_level(logging.INFO)

+         self.toddler_cls.find_and_remove(config)

+         assert "No members to remove from group group1" in caplog.text

+         self.mock_distgit.remove_member_from_group.assert_not_called()

+ 

+     def test_some_members_not_packager_members(self, caplog):

+         """

+         If some distgit group members are not IPA packager members, they are removed.

+         """

+         self.mock_distgit.get_all_groups.return_value = ["group1"]

+         self.mock_distgit.get_group_members.return_value = [

+             "lenkaseg",

+             "samyak",

+             "aurelien",

+         ]

+         self.mock_ipa_session.Command.group_show.return_value = {

+             "result": {"member_user": ["lenkaseg"], "membermanager_user": ["aurelien"]}

+         }

+         config = {"watched_groups": ["packager"]}

+         caplog.set_level(logging.INFO)

+         self.toddler_cls.find_and_remove(config)

+         assert (

+             "Found 1 members to remove from distgit and IPA group group1" in caplog.text

+         )

+         # Only samyak should be removed since lenkaseg is a member and aurelien is a sponsor

+         self.mock_distgit.remove_member_from_group.assert_any_call("samyak", "group1")

+         assert self.mock_distgit.remove_member_from_group.call_count == 1

+ 

+     def test_no_distgit_groups(self, caplog):

+         """

+         If no distgit groups are found, nothing happens.

+         """

+         self.mock_distgit.get_all_groups.return_value = []

+         config = {"watched_groups": ["packager"]}

+         caplog.set_level(logging.INFO)

+         self.toddler_cls.find_and_remove(config)

+ 

+         expected_logs = [

+             "Starting find_and_remove process",

+             "Found 0 distgit groups",

+             "Found 0 unique packagers (members and sponsors) in IPA packager groups",

+             "Completed find_and_remove process",

+         ]

+         for expected_log in expected_logs:

+             assert expected_log in caplog.text

+ 

+         self.mock_distgit.get_group_members.assert_not_called()

+ 

+     def test_error_fetching_distgit_groups(self, caplog):

+         """

+         If an error occurs fetching distgit groups, logs error and stops.

+         """

+         self.mock_distgit.get_all_groups.side_effect = PagureError("fail")

+         config = {"watched_groups": ["packager"]}

+         caplog.set_level(logging.INFO)

+         self.toddler_cls.find_and_remove(config)

+         assert "Failed to get distgit groups: fail" in caplog.text

+         self.mock_distgit.get_group_members.assert_not_called()

+ 

+     def test_error_fetching_group_members(self, caplog):

+         """

+         If an error occurs fetching distgit group members, logs error and stops.

+         """

+         self.mock_distgit.get_all_groups.return_value = ["group1"]

+         self.mock_distgit.get_group_members.return_value = []

+         self.mock_ipa_session.Command.group_show.return_value = {

+             "result": {"member_user": ["lenkaseg"]}

+         }

+         config = {"watched_groups": ["packager"]}

+         caplog.set_level(logging.DEBUG)

+         self.toddler_cls.find_and_remove(config)

+         assert "No group members found in distgit group group1" in caplog.text

+         self.mock_distgit.remove_member_from_group.assert_not_called()

+ 

+     def test_error_fetching_ipa_group_members(self, caplog):

+         """

+         If an error occurs fetching IPA group members, logs error and continues.

+         """

+         self.mock_distgit.get_all_groups.return_value = ["group1"]

+         self.mock_ipa_session.Command.group_show.side_effect = Exception(

+             "IPA group fetch failed"

+         )

+         config = {"watched_groups": ["packager"]}

+         caplog.set_level(logging.INFO)

+         self.toddler_cls.find_and_remove(config)

+         assert (

+             "Failed to get members of IPA group packager: IPA group fetch failed"

+             in caplog.text

+         )

+         # Should still complete the process despite the error

+         assert "Completed find_and_remove process" in caplog.text

+ 

+     @pytest.mark.parametrize(

+         "output,sponsor_removal_exception,expected_log_message",

+         [

+             (

+                 {"completed": 1},

+                 None,

+                 "User lenkaseg removed from members of ipa group group1",

+             ),

+             (

+                 {"completed": 0},

+                 None,

+                 "Removing user lenkaseg from members of ipa group group1 was not "

+                 "successful, output:",

+             ),

+             (

+                 None,

+                 Exception("ipa removal failed"),

+                 "Error while removing user lenkaseg from ipa group group1",

+             ),

+             (

+                 None,

+                 None,

+                 "Error while removing user lenkaseg from members of ipa group group1, no output",

+             ),

+         ],

+         ids=[

+             "ipa_user_removal_successful",

+             "ipa_user_removal_failed",

+             "ipa_removal_exception",

+             "ipa_removal_no_output",

+         ],

+     )

+     def test_sponsor_removal_scenarios(

+         self,

+         output,

+         sponsor_removal_exception,

+         expected_log_message,

+         caplog,

+     ):

+         self.mock_distgit.get_all_groups.return_value = ["group1"]

+         self.mock_distgit.get_group_members.return_value = ["lenkaseg"]

+ 

+         # Mock the packager groups check (for find_and_remove) - user is NOT a valid packager

+         # This will be called when checking if user should be removed from distgit groups

+         def mock_group_show_side_effect(cn):

+             if cn == "packager":  # This is the watched group

+                 return {"result": {"member_user": [], "membermanager_user": []}}

+             elif cn == "group1":  # This is the specific group for IPA removal

+                 return {

+                     "result": {"member_user": ["lenkaseg"], "membermanager_user": []}

+                 }

+             else:

+                 return {"result": {"member_user": [], "membermanager_user": []}}

+ 

+         self.mock_ipa_session.Command.group_show.side_effect = (

+             mock_group_show_side_effect

+         )

+ 

+         if not output and sponsor_removal_exception:

+             self.mock_ipa_session.Command.group_remove_member.side_effect = (

+                 sponsor_removal_exception

+             )

+             self.mock_ipa_session.Command.group_remove_member_manager.side_effect = (

+                 sponsor_removal_exception

+             )

+         else:

+             group_remove_output = output

+             self.mock_ipa_session.Command.group_remove_member.return_value = (

+                 group_remove_output

+             )

+             self.mock_ipa_session.Command.group_remove_member_manager.return_value = (

+                 group_remove_output

+             )

+             if not sponsor_removal_exception:

+                 self.mock_ipa_session.Command.group_remove_member.side_effect = None

+                 self.mock_ipa_session.Command.group_remove_member_manager.side_effect = (

+                     None

+                 )

+ 

+         config = {"watched_groups": ["packager"]}

+         caplog.set_level(logging.INFO)

+         self.toddler_cls.find_and_remove(config)

+ 

+         if expected_log_message:

+             assert expected_log_message in caplog.text

+ 

+     @pytest.mark.parametrize(

+         "user_as_sponsor,sponsor_removal_output,expected_logs",

+         [

+             (

+                 True,

+                 {"completed": 1},

+                 [

+                     "Removing user testuser from sponsors of ipa group group1",

+                     "User testuser removed from sponsors of ipa group group1",

+                 ],

+             ),

+             (

+                 True,

+                 {"completed": 0},

+                 [

+                     "Removing user testuser from sponsors of ipa group group1",

+                     "Removing user testuser from sponsors of ipa group group1 "

+                     "was not successful, output:",

+                 ],

+             ),

+             (

+                 False,

+                 None,

+                 [

+                     "User testuser is not a sponsor of ipa group group1, skipping sponsor removal",

+                 ],

+             ),

+         ],

+         ids=[

+             "sponsor_removal_successful",

+             "sponsor_removal_failed",

+             "user_not_sponsor",

+         ],

+     )

+     def test_sponsor_removal_scenarios_direct(

+         self,

+         user_as_sponsor,

+         sponsor_removal_output,

+         expected_logs,

+         caplog,

+     ):

+         """

+         Test sponsor removal scenarios for _remove_from_ipa method (lines 165-166).

+         """

+         self.mock_distgit.get_all_groups.return_value = ["group1"]

+         self.mock_distgit.get_group_members.return_value = ["testuser"]

+ 

+         # Set up group_show mock to indicate user sponsor status

+         if user_as_sponsor:

+             sponsors = ["testuser"]

+         else:

+             sponsors = []

+ 

+         def mock_group_show_side_effect(cn):

+             if cn == "packager":  # User is NOT a valid packager, so needs removal

+                 return {"result": {"member_user": [], "membermanager_user": []}}

+             elif cn == "group1":  # This is where we test sponsor status

+                 return {"result": {"member_user": [], "membermanager_user": sponsors}}

+             else:

+                 return {"result": {"member_user": [], "membermanager_user": []}}

+ 

+         self.mock_ipa_session.Command.group_show.side_effect = (

+             mock_group_show_side_effect

+         )

+ 

+         # Set up sponsor removal mock

+         if sponsor_removal_output:

+             self.mock_ipa_session.Command.group_remove_member_manager.return_value = (

+                 sponsor_removal_output

+             )

+ 

+         config = {"watched_groups": ["packager"]}

+         caplog.set_level(logging.INFO)

+         self.toddler_cls.find_and_remove(config)

+ 

+         # Check that all expected log messages are present

+         for expected_log in expected_logs:

+             assert expected_log in caplog.text

+ 

+         # Verify the appropriate IPA calls were made

+         if user_as_sponsor:

+             self.mock_ipa_session.Command.group_remove_member_manager.assert_called_once_with(

+                 cn="group1", user="testuser"

+             )

+         else:

+             self.mock_ipa_session.Command.group_remove_member_manager.assert_not_called()

@@ -134,7 +134,10 @@ 

      @patch("toddlers.utils.pagure.set_pagure")

      @patch("toddlers.utils.fedora_account.set_fasjson")

      @patch("toddlers.utils.bugzilla_system.set_bz")

-     def test_process_exception(self, mock_bugzilla, mock_fasjson, mock_pagure, toddler):

+     @patch("toddlers.utils.anitya.set_anitya")

+     def test_process_exception(

+         self, mock_anitya, mock_bugzilla, mock_fasjson, mock_pagure, toddler

+     ):

          """

          Assert that message toddler will be initialized correctly, if message passes

          initial processing.
@@ -180,12 +183,16 @@ 

          )

          mock_fasjson.assert_called_with(config)

          mock_bugzilla.assert_called_with(config)

+         mock_anitya.assert_called_with(config)

          mock_pagure_io.add_comment_to_issue.assert_called_once()

  

+     @patch("toddlers.utils.anitya.set_anitya")

      @patch("toddlers.utils.pagure.set_pagure")

      @patch("toddlers.utils.fedora_account.set_fasjson")

      @patch("toddlers.utils.bugzilla_system.set_bz")

-     def test_process(self, mock_bugzilla, mock_fasjson, mock_pagure, toddler):

+     def test_process(

+         self, mock_bugzilla, mock_fasjson, mock_pagure, mock_anitya, toddler

+     ):

          """

          Assert that message toddler will be initialized correctly, if message passes

          initial processing.
@@ -227,11 +234,15 @@ 

          )

          mock_fasjson.assert_called_with(config)

          mock_bugzilla.assert_called_with(config)

+         mock_anitya.assert_called_with(config)

  

+     @patch("toddlers.utils.anitya.set_anitya")

      @patch("toddlers.utils.pagure.set_pagure")

      @patch("toddlers.utils.fedora_account.set_fasjson")

      @patch("toddlers.utils.bugzilla_system.set_bz")

-     def test_process_comment(self, mock_bugzilla, mock_fasjson, mock_pagure, toddler):

+     def test_process_comment(

+         self, mock_bugzilla, mock_fasjson, mock_pagure, mock_anitya, toddler

+     ):

          """

          Assert that toddler will handle comments correctly.

          """
@@ -791,6 +802,7 @@ 

          self.toddler = scm_request_processor.SCMRequestProcessor()

          self.toddler.pagure_io = Mock()

          self.toddler.dist_git = Mock()

+         self.toddler.anitya = Mock()

          self.toddler.ping_comment = "{maintainers}"

  

      def test_process_new_repo_missing_required_key(self):
@@ -800,21 +812,32 @@ 

          issue = {

              "id": 100,

          }

-         self.toddler.process_new_repo(issue, {})

+         json = {

+             "repo": "+a",

+             "branch": "rawhide",

+             "namespace": "rpms",

+             "bug_id": "123",

+             "action": "new_repo",

+             "sls": {"rawhide": "2050-06-01"},

+             "monitor": "monitoring",

+         }

+         self.toddler.process_new_repo(issue, json)

  

          self.toddler.pagure_io.close_issue.assert_called_with(

              100,

              namespace=scm_request_processor.PROJECT_NAMESPACE,

-             message="Invalid body, missing required field: repo",

+             message="Invalid body, missing required field: upstreamurl",

              reason="Invalid",

          )

  

-     def test_process_new_repo_invalid_repo_name(self):

+     def test_process_new_repo_missing_required_key_for_monitor(self):

          """

-         Assert that ticket will be closed if provided repository name is invalid.

+         Assert that ticket will be closed if required key for monitor

+         is missing in request.

          """

-         issue = {"id": 100, "user": {"name": "zlopez"}}

- 

+         issue = {

+             "id": 100,

+         }

          json = {

              "repo": "+a",

              "branch": "rawhide",
@@ -822,40 +845,62 @@ 

              "bug_id": "123",

              "action": "new_repo",

              "sls": {"rawhide": "2050-06-01"},

-             "monitor": "monitor",

+             "monitor": "monitoring",

+             "upstreamurl": "",

+             "backend": "GitLab",

          }

- 

          self.toddler.process_new_repo(issue, json)

  

-         error = (

-             "The repository name is invalid. It must be at least two "

-             "characters long with only letters, numbers, hyphens, "

-             "underscores, plus signs, and/or periods. Please note that "

-             "the project cannot start with a period or a plus sign. "

-             "Repository name can't be longer than 64 characters."

+         self.toddler.pagure_io.close_issue.assert_called_with(

+             100,

+             namespace=scm_request_processor.PROJECT_NAMESPACE,

+             message="Invalid body, missing required field: project_name",

+             reason="Invalid",

          )

  

+     def test_process_new_repo_monitor_accepts_different_options(self):

+         """

+         Assert that ticket will be closed if required key for monitor

+         is missing in request.

+         """

+         issue = {

+             "id": 100,

+         }

+         json = {

+             "repo": "+a",

+             "branch": "rawhide",

+             "namespace": "rpms",

+             "bug_id": "123",

+             "action": "new_repo",

+             "sls": {"rawhide": "2050-06-01"},

+             "monitor": "monitoring11",

+             "upstreamurl": "",

+             "backend": "GitLab",

+         }

+         self.toddler.process_new_repo(issue, json)

+ 

          self.toddler.pagure_io.close_issue.assert_called_with(

              100,

              namespace=scm_request_processor.PROJECT_NAMESPACE,

-             message=error,

+             message="Invalid body, missing required field: project_name",

              reason="Invalid",

          )

  

-     def test_process_new_repo_long_repo_name(self):

+     def test_process_new_repo_invalid_repo_name(self):

          """

          Assert that ticket will be closed if provided repository name is invalid.

          """

          issue = {"id": 100, "user": {"name": "zlopez"}}

  

          json = {

-             "repo": "".join("a" for _ in range(65)),

+             "repo": "+a",

              "branch": "rawhide",

              "namespace": "rpms",

              "bug_id": "123",

              "action": "new_repo",

              "sls": {"rawhide": "2050-06-01"},

-             "monitor": "monitor",

+             "monitor": "no-monitoring",

+             "upstreamurl": "",

          }

  

          self.toddler.process_new_repo(issue, json)
@@ -888,7 +933,8 @@ 

              "bug_id": "",

              "action": "new_repo",

              "sls": {"rawhide": "2050-06-01"},

-             "monitor": "monitor",

+             "monitor": "no-monitoring",

+             "upstreamurl": "",

          }

  

          self.toddler.dist_git.get_project.return_value = None
@@ -917,7 +963,8 @@ 

              "bug_id": "123",

              "action": "new_repo",

              "sls": {"rawhide": "2050-06-01"},

-             "monitor": "monitor",

+             "monitor": "no-monitoring",

+             "upstreamurl": "",

          }

  

          self.toddler.dist_git.get_project.return_value = None
@@ -951,7 +998,8 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {"rawhide": "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

+         upstreamurl = ""

          exception = False

          json = {

              "repo": repo,
@@ -961,6 +1009,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": upstreamurl,

              "exception": exception,

          }

  
@@ -990,7 +1039,8 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {"rawhide": "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

+         upstreamurl = ""

          exception = False

          json = {

              "repo": repo,
@@ -1000,6 +1050,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": upstreamurl,

              "exception": exception,

          }

  
@@ -1020,7 +1071,10 @@ 

              comment=message,

          )

  

-     def test_process_new_repo_master_branch(self):

+     @patch(

+         "toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"

+     )

+     def test_process_new_repo_master_branch(self, mock_validate_review_bug):

          """

          Assert that ticket will be closed when branch is set to master branch.

          Master branch is no longer allowed.
@@ -1033,7 +1087,8 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {"rawhide": "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

+         upstreamurl = ""

          exception = False

          json = {

              "repo": repo,
@@ -1043,6 +1098,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": upstreamurl,

              "exception": exception,

          }

          self.toddler.dist_git.get_project.return_value = None
@@ -1068,7 +1124,8 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {"rawhide": "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

+         upstreamurl = ""

          exception = False

          json = {

              "repo": repo,
@@ -1078,6 +1135,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": upstreamurl,

              "exception": exception,

          }

          self.toddler.process_new_repo(issue, json)
@@ -1120,11 +1178,12 @@ 

          }

  

          repo = "repo"

-         bug_id = ""

+         bug_id = "11"

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

-         exception = True

+         monitor = "no-monitoring"

+         upstreamurl = ""

+         exception = False

          json = {

              "repo": repo,

              "branch": branch,
@@ -1133,6 +1192,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": upstreamurl,

              "exception": exception,

          }

          dist_git_url = "https://src.fp.o"
@@ -1141,9 +1201,12 @@ 

          self.toddler.pagure_io.get_project_contributors.return_value = {

              "users": {"admin": [user], "commit": [], "ticket": []}

          }

+         self.toddler.validation_comment = "valid"

+         self.toddler.validate_review_bug = Mock()

  

          # Method to test

-         self.toddler.process_new_repo(issue, json)

+         with patch("toddlers.plugins.scm_request_processor.bugzilla_system"):

+             self.toddler.process_new_repo(issue, json)

  

          # asserts

          self.toddler.pagure_io.add_comment_to_issue.assert_called_with(
@@ -1176,7 +1239,8 @@ 

          bug_id = ""

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

+         upstreamurl = ""

          exception = True

          json = {

              "repo": repo,
@@ -1186,6 +1250,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": upstreamurl,

              "exception": exception,

          }

          dist_git_url = "https://src.fp.o"
@@ -1203,9 +1268,10 @@ 

  

      @patch("toddlers.plugins.scm_request_processor.bugzilla_system")

      @patch(

-         "toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"

+         "toddlers.plugins.scm_request_processor.SCMRequestProcessor._validate_new_repo_request",

+         return_value=True,

      )

-     def test_process_new_repo_project_exists(self, mock_validate_review_bug, mock_bz):

+     def test_process_new_repo_project_exists(self, mock_validate_request, mock_bz):

          """

          Assert that ticket will be processed correctly when repo already

          exists in dist git.
@@ -1218,7 +1284,7 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {"rawhide": "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

          exception = False

          json = {

              "repo": repo,
@@ -1229,6 +1295,7 @@ 

              "sls": sls,

              "monitor": monitor,

              "exception": exception,

+             "upstreamurl": "",

          }

  

          dist_git_url = "https://src.fp.o"
@@ -1270,7 +1337,7 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

          exception = False

          json = {

              "repo": repo,
@@ -1281,6 +1348,7 @@ 

              "sls": sls,

              "monitor": monitor,

              "exception": exception,

+             "upstreamurl": "",

          }

          self.toddler.branch_slas = {"rawhide": {"rawhide": "2050-06-01"}}

  
@@ -1322,6 +1390,314 @@ 

          )

          mock_bz.change_bug_status.assert_called_with(bug_id, "RELEASE_PENDING", message)

  

+     @patch(

+         "toddlers.plugins.scm_request_processor.SCMRequestProcessor._validate_new_repo_request",

+         return_value=True,

+     )

+     def test_process_new_repo_monitoring_project_created_successfully_package_exist(

+         self,

+         mock_validate_request,

+     ):

+         """

+         Assert that ticket will be processed with correct Monitoring message

+         when project and package exists.

+         """

+         # Preparation

+         user = "zlopez"

+         issue = {

+             "id": 100,

+             "user": {"name": user},

+         }

+ 

+         repo = "repo"

+         branch = "main"

+         namespace = "tests"

+         bug_id = ""

+         action = "new_repo"

+         sls = {branch: "2050-06-01"}

+         monitor = "monitoring"

+         upstreamurl = ""

+         backend = "custom"

+         distibution = "Fedora"

+         project_name = "test_project"

+         exception = False

+         json = {

+             "repo": repo,

+             "branch": branch,

+             "namespace": namespace,

+             "bug_id": bug_id,

+             "action": action,

+             "sls": sls,

+             "monitor": monitor,

+             "upstreamurl": upstreamurl,

+             "backend": backend,

+             "distribution": distibution,

+             "project_name": project_name,

+             "exception": exception,

+         }

+         dist_git_url = "https://src.fp.o"

+         self.toddler.dist_git._pagure_url = dist_git_url

+         self.toddler.dist_git.get_project.return_value = {"access_users": {"owner": []}}

+         anitya_project_url = "https://release-monitoring.org/project/123"

+         self.toddler.anitya.does_project_exists_in_anitya = Mock(

+             return_value=anitya_project_url

+         )

+         self.toddler.anitya.does_package_exists_in_anitya = Mock(return_value=True)

+         project_msg = (

+             "Anitya project is accessible by this link \n`{0}`\n "

+             "you can modify it manually.".format(anitya_project_url)

+         )

+ 

+         self.toddler.process_new_repo(issue, json)

+ 

+         self.toddler.dist_git.set_monitoring_status.assert_called_with(

+             namespace, repo, monitor

+         )

+         monitoring_msg = "\nMonitoring:\n{0}\n".format(project_msg)

+ 

+         message = "The Pagure repository was created at {0}/{1}/{2}{3}".format(

+             dist_git_url, namespace, repo, monitoring_msg

+         )

+ 

+         self.toddler.pagure_io.close_issue.assert_called_with(

+             100,

+             namespace=scm_request_processor.PROJECT_NAMESPACE,

+             message=message,

+             reason="Processed",

+         )

+ 

+     @patch(

+         "toddlers.plugins.scm_request_processor.SCMRequestProcessor._validate_new_repo_request",

+         return_value=True,

+     )

+     def test_process_new_repo_monitoring_project_was_not_created(

+         self,

+         mock_validate_request,

+     ):

+         """

+         Assert that ticket will be processed with correct Monitoring message

+         when project and package exists.

+         """

+         # Preparation

+         user = "zlopez"

+         issue = {

+             "id": 100,

+             "user": {"name": user},

+         }

+ 

+         repo = "repo"

+         branch = "main"

+         namespace = "tests"

+         bug_id = ""

+         action = "new_repo"

+         sls = {branch: "2050-06-01"}

+         monitor = "monitoring"

+         upstreamurl = ""

+         backend = "custom"

+         distibution = "Fedora"

+         project_name = "test_project"

+         exception = False

+         json = {

+             "repo": repo,

+             "branch": branch,

+             "namespace": namespace,

+             "bug_id": bug_id,

+             "action": action,

+             "sls": sls,

+             "monitor": monitor,

+             "upstreamurl": upstreamurl,

+             "backend": backend,

+             "distribution": distibution,

+             "project_name": project_name,

+             "exception": exception,

+         }

+         dist_git_url = "https://src.fp.o"

+         self.toddler.dist_git._pagure_url = dist_git_url

+         self.toddler.dist_git.get_project.return_value = {"access_users": {"owner": []}}

+         self.toddler.anitya.does_project_exists_in_anitya = Mock(return_value=None)

+         self.toddler.anitya.create_project_in_anitya = Mock(return_value=None)

+         project_msg = (

+             "Wasn't able to create project in Anitya. "

+             "You can create it manually on: `https://release-monitoring.org`"

+         )

+ 

+         self.toddler.process_new_repo(issue, json)

+ 

+         self.toddler.dist_git.set_monitoring_status.assert_called_with(

+             namespace, repo, monitor

+         )

+         monitoring_msg = "\nMonitoring:\n{0}\n".format(project_msg)

+ 

+         message = "The Pagure repository was created at {0}/{1}/{2}{3}".format(

+             dist_git_url, namespace, repo, monitoring_msg

+         )

+ 

+         self.toddler.pagure_io.close_issue.assert_called_with(

+             100,

+             namespace=scm_request_processor.PROJECT_NAMESPACE,

+             message=message,

+             reason="Processed",

+         )

+ 

+     @patch(

+         "toddlers.plugins.scm_request_processor.SCMRequestProcessor._validate_new_repo_request",

+         return_value=True,

+     )

+     def test_process_new_repo_monitoring_creating_package(

+         self,

+         mock_validate_request,

+     ):

+         """

+         Assert that ticket will be processed with correct Monitoring message

+         when project and package exists.

+         """

+         # Preparation

+         user = "zlopez"

+         issue = {

+             "id": 100,

+             "user": {"name": user},

+         }

+ 

+         repo = "repo"

+         branch = "main"

+         namespace = "tests"

+         bug_id = ""

+         action = "new_repo"

+         sls = {branch: "2050-06-01"}

+         monitor = "monitoring"

+         upstreamurl = ""

+         backend = "custom"

+         distibution = "Fedora"

+         project_name = "test_project"

+         exception = False

+         json = {

+             "repo": repo,

+             "branch": branch,

+             "namespace": namespace,

+             "bug_id": bug_id,

+             "action": action,

+             "sls": sls,

+             "monitor": monitor,

+             "upstreamurl": upstreamurl,

+             "backend": backend,

+             "distribution": distibution,

+             "project_name": project_name,

+             "exception": exception,

+         }

+         dist_git_url = "https://src.fp.o"

+         self.toddler.dist_git._pagure_url = dist_git_url

+         self.toddler.dist_git.get_project.return_value = {"access_users": {"owner": []}}

+         anitya_project_url = "https://release-monitoring.org/project/123"

+         self.toddler.anitya.does_project_exists_in_anitya = Mock(

+             return_value=anitya_project_url

+         )

+         self.toddler.anitya.does_package_exists_in_anitya = Mock(return_value=False)

+         self.toddler.anitya.create_package_in_anitya = Mock(return_value="Success")

+         project_msg = (

+             "Anitya project is accessible by this link \n`{0}`\n "

+             "you can modify it manually.".format(anitya_project_url)

+         )

+         package_msg = "Package was created in Anitya"

+ 

+         self.toddler.process_new_repo(issue, json)

+ 

+         self.toddler.dist_git.set_monitoring_status.assert_called_with(

+             namespace, repo, monitor

+         )

+         monitoring_msg = "\nMonitoring:\n{0}\n{1}".format(project_msg, package_msg)

+ 

+         message = "The Pagure repository was created at {0}/{1}/{2}{3}".format(

+             dist_git_url, namespace, repo, monitoring_msg

+         )

+ 

+         self.toddler.pagure_io.close_issue.assert_called_with(

+             100,

+             namespace=scm_request_processor.PROJECT_NAMESPACE,

+             message=message,

+             reason="Processed",

+         )

+ 

+     @patch(

+         "toddlers.plugins.scm_request_processor.SCMRequestProcessor._validate_new_repo_request",

+         return_value=True,

+     )

+     def test_process_new_repo_monitoring_creating_package_fails(

+         self,

+         mock_validate_request,

+     ):

+         """

+         Assert that ticket will be processed with correct Monitoring message

+         when project and package exists.

+         """

+         # Preparation

+         user = "zlopez"

+         issue = {

+             "id": 100,

+             "user": {"name": user},

+         }

+ 

+         repo = "repo"

+         branch = "main"

+         namespace = "tests"

+         bug_id = ""

+         action = "new_repo"

+         sls = {branch: "2050-06-01"}

+         monitor = "monitoring"

+         upstreamurl = ""

+         backend = "custom"

+         distibution = "Fedora"

+         project_name = "test_project"

+         exception = False

+         json = {

+             "repo": repo,

+             "branch": branch,

+             "namespace": namespace,

+             "bug_id": bug_id,

+             "action": action,

+             "sls": sls,

+             "monitor": monitor,

+             "upstreamurl": upstreamurl,

+             "backend": backend,

+             "distribution": distibution,

+             "project_name": project_name,

+             "exception": exception,

+         }

+         dist_git_url = "https://src.fp.o"

+         self.toddler.dist_git._pagure_url = dist_git_url

+         self.toddler.dist_git.get_project.return_value = {"access_users": {"owner": []}}

+         anitya_project_url = "https://release-monitoring.org/project/123"

+         self.toddler.anitya.does_project_exists_in_anitya = Mock(

+             return_value=anitya_project_url

+         )

+         self.toddler.anitya.does_package_exists_in_anitya = Mock(return_value=False)

+         response_msg = "Unauthorized, access token is incorrect."

+         self.toddler.anitya.create_package_in_anitya = Mock(return_value=response_msg)

+         project_msg = (

+             "Anitya project is accessible by this link \n`{0}`\n "

+             "you can modify it manually.".format(anitya_project_url)

+         )

+         package_msg = "Package wasn't created in Anitya, reason: `{0}`.".format(

+             response_msg

+         )

+ 

+         self.toddler.process_new_repo(issue, json)

+ 

+         self.toddler.dist_git.set_monitoring_status.assert_called_with(

+             namespace, repo, monitor

+         )

+         monitoring_msg = "\nMonitoring:\n{0}\n{1}".format(project_msg, package_msg)

+ 

+         message = "The Pagure repository was created at {0}/{1}/{2}{3}".format(

+             dist_git_url, namespace, repo, monitoring_msg

+         )

+ 

+         self.toddler.pagure_io.close_issue.assert_called_with(

+             100,

+             namespace=scm_request_processor.PROJECT_NAMESPACE,

+             message=message,

+             reason="Processed",

+         )

+ 

      @patch("toddlers.plugins.scm_request_processor.bugzilla_system")

      @patch(

          "toddlers.plugins.scm_request_processor.SCMRequestProcessor.validate_review_bug"
@@ -1341,7 +1717,8 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

+         upstreamurl = ""

          exception = False

          json = {

              "repo": repo,
@@ -1351,6 +1728,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": upstreamurl,

              "exception": exception,

          }

          self.toddler.branch_slas = {"rawhide": {"rawhide": "2050-06-01"}}
@@ -1411,7 +1789,8 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

+         upstreamurl = ""

          exception = False

          json = {

              "repo": repo,
@@ -1421,6 +1800,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": upstreamurl,

              "exception": exception,

          }

  
@@ -1473,7 +1853,7 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

          exception = False

          json = {

              "repo": repo,
@@ -1483,6 +1863,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": "",

              "exception": exception,

          }

  
@@ -1542,7 +1923,7 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

          exception = False

          json = {

              "repo": repo,
@@ -1552,6 +1933,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": "",

              "exception": exception,

          }

  
@@ -1627,7 +2009,7 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

          exception = False

          json = {

              "repo": repo,
@@ -1637,6 +2019,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": "",

              "exception": exception,

          }

  
@@ -1703,7 +2086,7 @@ 

          bug_id = "123"

          action = "new_repo"

          sls = {branch: "2050-06-01"}

-         monitor = "monitor"

+         monitor = "no-monitoring"

          exception = False

          json = {

              "repo": repo,
@@ -1713,6 +2096,7 @@ 

              "action": action,

              "sls": sls,

              "monitor": monitor,

+             "upstreamurl": "",

              "exception": exception,

          }

  

The added file is too large to be shown here, see it at: tests/plugins/test_unretire_packages.py
file modified
+12
@@ -9,6 +9,18 @@ 

  from toddlers.utils.misc import merge_dicts

  

  

+ @pytest.fixture(scope="session", autouse=True)

+ def patch_ipa_api():

+     """Patch IPA API globally to prevent bootstrap() already called error."""

+     patcher = patch("toddlers.plugins.cleaning_packager_groups.ipalib.api")

+     mock_api = patcher.start()

+     mock_api.bootstrap.return_value = None

+     mock_api.load_plugins.return_value = None

+     mock_api.finalize.return_value = None

+     yield

+     patcher.stop()

+ 

+ 

  def patch_messaging_config(*add_dicts):

      mock_config = {

          "consumer_config": {"blocked_toddlers": ["debug"], "toddlers": {}},

@@ -0,0 +1,410 @@ 

+ """

+ Unit tests for `toddlers.utils.anitya`.

+ """

+ 

+ from unittest.mock import Mock

+ 

+ import pytest

+ 

+ import toddlers.utils.anitya as anitya

+ 

+ 

+ class TestAnityaSetAnitya:

+     """

+     Test class for `toddlers.anitya.set_anitya` function.

+     """

+ 

+     def test_set_anitya(self):

+         """

+         Test initialization of anitya module.

+         """

+         config = {

+             "anitya_endpoint": "https://release-monitoring.org",

+             "anitya_access_token": "TOKEN",

+         }

+         anitya_obj = anitya.set_anitya(config)

+ 

+         assert anitya_obj._anitya_endpoint == config.get("anitya_endpoint")

+         assert anitya_obj._anitya_token == config.get("anitya_access_token")

+         assert anitya_obj._requests_session

+ 

+     def test_set_anitya_no_anitya_url(self):

+         """

+         Test initialization of anitya module without required config value.

+         """

+         with pytest.raises(

+             ValueError, match=r"No anitya endpoint found in config file"

+         ):

+             anitya.set_anitya({})

+ 

+     def test_set_anitya_no_anitya_api_key(self):

+         """

+         Test initialization of anitya module without required config value.

+         """

+         with pytest.raises(

+             ValueError, match=r"No anitya access token found in config file"

+         ):

+             config = {"anitya_endpoint": "https://anitya.io"}

+             anitya.set_anitya(config)

+ 

+ 

+ class TestAnityaDoesProjectExistInAnitya:

+     """

+     Test class for

+     `toddlers.anitya.Anitya.does_project_exists_in_anitya` method.

+     """

+ 

+     def setup_method(self):

+         """

+         Setup method for test class.

+         """

+         config = {

+             "anitya_endpoint": "https://release-monitoring.org",

+             "anitya_access_token": "TOKEN",

+         }

+         self.anitya_obj = anitya.set_anitya(config)

+         self.anitya_obj._requests_session = Mock()

+         self.anitya_obj.remove_trailing_slashes_from_url = Mock(

+             return_value="https://release-monitoring.org/api/v2/projects/"

+         )

+ 

+     def test_does_project_exists_in_anitya(self):

+         """

+         Assert that method will return correct response about project exists in anitya.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/projects/"

+         project_name = "amedvede_project"

+         mock_response = Mock()

+         mock_response.status_code = 200

+         mock_response.json.return_value = {

+             "items": [

+                 {

+                     "id": 123,

+                     "name": project_name,

+                 }

+             ],

+             "total_items": 1,

+         }

+         params = {"name": project_name}

+         self.anitya_obj._requests_session.get.return_value = mock_response

+ 

+         result = self.anitya_obj.does_project_exists_in_anitya(project_name)

+ 

+         assert result == "https://release-monitoring.org/project/123"

+         self.anitya_obj._requests_session.get.assert_called_once_with(

+             endpoint, params=params

+         )

+ 

+     def test_does_project_exists_in_anitya_project_not_found(self):

+         """

+         Assert that method will return correct response about project does not exist in anitya.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/projects/"

+         project_name = "amedvede_project"

+         mock_response = Mock()

+         mock_response.status_code = 404

+         params = {"name": project_name}

+         self.anitya_obj._requests_session.get.return_value = mock_response

+ 

+         result = self.anitya_obj.does_project_exists_in_anitya(project_name)

+ 

+         assert result is None

+         self.anitya_obj._requests_session.get.assert_called_once_with(

+             endpoint, params=params

+         )

+ 

+     def test_does_project_exists_in_anitya_empty_items(self):

+         """Assert that method will return correct response about project not found in anitya."""

+         endpoint = "https://release-monitoring.org/api/v2/projects/"

+         project_name = "amedvede_project"

+         mock_response = Mock()

+         mock_response.status_code = 200

+         mock_response.json.return_value = {"items": [], "total_items": 0}

+         params = {"name": project_name}

+         self.anitya_obj._requests_session.get.return_value = mock_response

+ 

+         result = self.anitya_obj.does_project_exists_in_anitya(project_name)

+ 

+         assert result is None

+         self.anitya_obj._requests_session.get.assert_called_once_with(

+             endpoint, params=params

+         )

+ 

+     def test_does_project_exists_in_anitya_wrong_structure(self):

+         """

+         Assert that method will return correct response about project has wrong structure in anitya.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/projects/"

+         project_name = "amedvede_project"

+         mock_response = Mock()

+         mock_response.status_code = 200

+         mock_response.json.return_value = {

+             "items": [

+                 {

+                     "wrong": "structure",

+                 }

+             ],

+             "total_items": 1,

+         }

+         params = {"name": project_name}

+         self.anitya_obj._requests_session.get.return_value = mock_response

+ 

+         result = self.anitya_obj.does_project_exists_in_anitya(project_name)

+ 

+         assert result is None

+         self.anitya_obj._requests_session.get.assert_called_once_with(

+             endpoint, params=params

+         )

+ 

+ 

+ class TestAnityaDoesPackageExistInAnitya:

+     """

+     Test class for `toddlers.anitya.Anitya.does_package_exists_in_anitya` method.

+     """

+ 

+     def setup_method(self):

+         """

+         Setup method for test class.

+         """

+         config = {

+             "anitya_endpoint": "https://release-monitoring.org",

+             "anitya_access_token": "TOKEN",

+         }

+         self.anitya_obj = anitya.set_anitya(config)

+         self.anitya_obj._requests_session = Mock()

+         self.anitya_obj.remove_trailing_slashes_from_url = Mock(

+             return_value="https://release-monitoring.org/api/v2/packages/"

+         )

+ 

+     @pytest.mark.parametrize(

+         "project_name, expected_project_name, expected_result",

+         [

+             ("nice_project", "nice_project", True),

+             ("nice_project", "bad_project", False),

+         ],

+     )

+     def test_does_package_exists_in_anitya(

+         self, project_name, expected_project_name, expected_result

+     ):

+         """

+         Assert that method will return correct response about package exists in anitya

+         and his project name is same with expected.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/packages/"

+         package_name = "amedvede_package"

+         distribution = "Fedora"

+         mock_response = Mock()

+         mock_response.status_code = 200

+         mock_response.json.return_value = {

+             "items": [

+                 {

+                     "name": package_name,

+                     "project": project_name,

+                 }

+             ],

+             "total_items": 1,

+         }

+         params = {

+             "name": package_name,

+             "distribution": distribution,

+         }

+         self.anitya_obj._requests_session.get.return_value = mock_response

+ 

+         result = self.anitya_obj.does_package_exists_in_anitya(

+             package_name, distribution, expected_project_name

+         )

+ 

+         assert result is expected_result  # package and project name the same

+         self.anitya_obj._requests_session.get.assert_called_once_with(

+             endpoint, params=params

+         )

+ 

+     def test_does_package_exists_in_anitya_not_found(self):

+         """

+         Assert that method will return correct response when package does not exist in anitya.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/packages/"

+         package_name = "amedvede_package"

+         project_name = "different name"

+         distribution = "Fedora"

+         mock_response = Mock()

+         mock_response.status_code = 202

+         params = {

+             "name": package_name,

+             "distribution": distribution,

+         }

+         self.anitya_obj._requests_session.get.return_value = mock_response

+ 

+         result = self.anitya_obj.does_package_exists_in_anitya(

+             package_name, distribution, project_name

+         )

+ 

+         assert result is False  # package and project name are different

+         self.anitya_obj._requests_session.get.assert_called_once_with(

+             endpoint, params=params

+         )

+ 

+     def test_does_package_exists_in_anitya_found_zero_items(self):

+         """

+         Assert that method will return correct response when response code is correct,

+         but response does not contain items.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/packages/"

+         package_name = "amedvede_package"

+         project_name = "different name"

+         distribution = "Fedora"

+         mock_response = Mock()

+         mock_response.status_code = 200

+         mock_response.json.return_value = {"items": [], "total_items": 0}

+         params = {

+             "name": package_name,

+             "distribution": distribution,

+         }

+         self.anitya_obj._requests_session.get.return_value = mock_response

+ 

+         result = self.anitya_obj.does_package_exists_in_anitya(

+             package_name, distribution, project_name

+         )

+ 

+         assert result is False  # package and project name are different

+         self.anitya_obj._requests_session.get.assert_called_once_with(

+             endpoint, params=params

+         )

+ 

+ 

+ class TestAnityaCreateProjectInAnitya:

+     """

+     Test class for `toddlers.anitya.Anitya.create_project_in_anitya` method.

+     """

+ 

+     def setup_method(self):

+         """

+         Setup method for test class.

+         """

+         config = {

+             "anitya_endpoint": "https://release-monitoring.org",

+             "anitya_access_token": "TOKEN",

+         }

+         self.anitya_obj = anitya.set_anitya(config)

+         self.anitya_obj._requests_session = Mock()

+         self.anitya_obj.remove_trailing_slashes_from_url = Mock(

+             return_value="https://release-monitoring.org/api/v2/projects/"

+         )

+ 

+     def test_create_project_in_anitya_successful_creation(self):

+         """

+         Assert that method will return correct response when project is created.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/projects/"

+         project_name = "project"

+         homepage = "https://project.com"

+         backend = "GitHub"

+         test_data = {

+             "name": project_name,

+             "homepage": homepage,

+             "backend": backend,

+         }

+         response_json = {"id": 123}

+         mock_response = Mock()

+         mock_response.status_code = 201

+         mock_response.json.return_value = response_json

+         self.anitya_obj._requests_session.post.return_value = mock_response

+ 

+         result = self.anitya_obj.create_project_in_anitya(

+             project_name, homepage, backend

+         )

+ 

+         assert result == "https://release-monitoring.org/project/123"

+         self.anitya_obj._requests_session.post.assert_called_once_with(

+             url=endpoint,

+             data=test_data,

+             headers={"Authorization": "token TOKEN"},

+         )

+ 

+     def test_create_project_in_anitya_fail(self):

+         """

+         Assert that method will return correct response when project is not created.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/projects/"

+         project_name = "project"

+         homepage = "https://project.com"

+         backend = "GitHub"

+         test_data = {

+             "name": project_name,

+             "homepage": homepage,

+             "backend": backend,

+         }

+         mock_response = Mock()

+         mock_response.status_code = 400

+         self.anitya_obj._requests_session.post.return_value = mock_response

+ 

+         result = self.anitya_obj.create_project_in_anitya(

+             project_name, homepage, backend

+         )

+ 

+         assert result is None

+         self.anitya_obj._requests_session.post.assert_called_once_with(

+             url=endpoint,

+             data=test_data,

+             headers={"Authorization": "token TOKEN"},

+         )

+ 

+ 

+ class TestAnityaCreatePackageInAnitya:

+     """

+     Test class for `toddlers.anitya.Anitya.create_package_in_anitya` method.

+     """

+ 

+     def setup_method(self):

+         """

+         Setup method for test class.

+         """

+         config = {

+             "anitya_endpoint": "https://release-monitoring.org",

+             "anitya_access_token": "TOKEN",

+         }

+         self.anitya_obj = anitya.set_anitya(config)

+         self.anitya_obj._requests_session = Mock()

+         self.anitya_obj.remove_trailing_slashes_from_url = Mock(

+             return_value="https://release-monitoring.org/api/v2/packages/"

+         )

+ 

+     @pytest.mark.parametrize(

+         "response_code, expected_result",

+         [

+             (201, "Success"),

+             (400, "Bad Request, some necessary arguments were not provided."),

+             (401, "Unauthorized, access token is incorrect."),

+             (409, "Conflict, package already exists."),

+             (404, None),

+         ],

+     )

+     def test_create_package_in_anitya(self, response_code, expected_result):

+         """

+         Assert that method will return correct response when package is created.

+         """

+         endpoint = "https://release-monitoring.org/api/v2/packages/"

+         package_name = "test_package"

+         project_name = "test_project"

+         distribution = "Fedora"

+         project_ecosystem = "https://project.com"

+         test_data = {

+             "package_name": package_name,

+             "project_name": project_name,

+             "distribution": distribution,

+             "project_ecosystem": project_ecosystem,

+         }

+         mock_response = Mock()

+         mock_response.status_code = response_code

+         self.anitya_obj._requests_session.post.return_value = mock_response

+ 

+         result = self.anitya_obj.create_package_in_anitya(

+             package_name, project_name, distribution, project_ecosystem

+         )

+ 

+         assert result == expected_result

+         self.anitya_obj._requests_session.post.assert_called_once_with(

+             url=endpoint,

+             data=test_data,

+             headers={"Authorization": "token TOKEN"},

+         )

file modified
+14 -10
@@ -2,7 +2,7 @@ 

  Unit tests for `toddlers.utils.git`.

  """

  

- from unittest.mock import call, MagicMock, Mock, patch

+ from unittest.mock import MagicMock, Mock, patch

  

  import pytest

  
@@ -290,24 +290,28 @@ 

          Assert that revert last commit process correctly.

          """

          mock_origin = MagicMock()

+         mock_origin.url = "https://example.com"

          self.repo.repo.remote.return_value = mock_origin

+         mock_git_cmd = MagicMock()

+         self.repo.repo.git = mock_git_cmd

  

-         self.repo.revert_last_commit("Revert message", "feature_branch")

+         self.repo.revert_last_commit("Revert message", "bot", "token", "feature_branch")

          self.repo.repo.git.checkout.assert_called_once_with("feature_branch")

-         self.repo.repo.git.execute.assert_has_calls(

-             [

-                 call(["git", "revert", "--no-edit", "HEAD"]),

-                 call(["git", "commit", "--amend", "-m", "Revert message"]),

-             ]

+         self.repo.repo.git.revert.assert_called_once_with("HEAD", no_edit=True)

+         self.repo.repo.git.commit.assert_called_once_with(

+             "--amend", "-m", "Revert message"

+         )

+ 

+         mock_git_cmd.push.assert_called_once_with(

+             "-u", "https://bot:token@example.com", "feature_branch"

          )

-         mock_origin.push.assert_called_once()

  

      def test_revert_last_commit_revert_exception(self):

          mock_origin = MagicMock()

          self.repo.repo.remote.return_value = mock_origin

-         self.repo.repo.git.execute.side_effect = Exception("Revert error")

+         self.repo.repo.git.revert.side_effect = Exception("Revert error")

  

-         self.repo.revert_last_commit("Revert message", "feature_branch")

+         self.repo.revert_last_commit("Revert message", "bot", "token", "feature_branch")

          mock_origin.push.assert_not_called()

  

  

file modified
+3
@@ -253,6 +253,9 @@ 

  ping_comment = "This request wants to skip bugzilla validation! {maintainers} could you check if this is correct? If yes, please respond to this ticket with 'valid' comment"

  # This is a OIDC token that allows pagure_user to push changes to dist git

  oidc_distgit_token = "OIDC token used to push git changes using pagure_user"

+ # Anitya access token and endpoint for managing project in release-monitoring

+ anitya_access_token = "API token for Anitya"

+ anitya_endpoint = "https://release-monitoring.org"

  

  

  # Pagure mapping to bugzilla

@@ -17,12 +17,18 @@ 

  _log = logging.getLogger(__name__)

  

  

+ class IPAError(Exception):

+     def __init__(self, message):

+         self.message = message

+ 

+ 

  class CleanPackagerGroups(ToddlerBase):

      """Listens to messages to check for a membership removal message from FAS"""

  

      name: str = "clean_packagers_groups"

      amqp_topics: list[str] = [

          "org.fedoraproject.*.ipa.group_remove_member.v1",

+         "org.fedoraproject.*.toddlers.trigger.clean_packagers_groups",

      ]

      pagure: pagure.Pagure

  
@@ -30,37 +36,29 @@ 

          super().__init__()

          self.pagure_url = None

          self.dist_git = None

-         self._ipa_session = None

+         # Initialize IPA session

+         conf_vars = ("KRB5_CONFIG", "IPA_CONFDIR")

+         for conf_var in conf_vars:

+             if not self._config.get(conf_var):

+                 continue

+             os.environ[conf_var] = self._config[conf_var]  # pragma: no cover

+         self._ipa_session = ipalib.api

+         self._ipa_session.bootstrap(context="custom")

+         self._ipa_session.load_plugins()

+         self._ipa_session.finalize()

  

      def accepts_topic(self, topic):

          """Returns a boolean whether this toddler is interested in messages

          from this specific topic.

          """

-         return topic.startswith("org.fedoraproject.") and topic.endswith(

-             "ipa.group_remove_member.v1"

+         return topic.startswith("org.fedoraproject.") and (

+             topic.endswith("ipa.group_remove_member.v1")

+             or topic.endswith("toddlers.trigger.clean_packagers_groups")

          )

  

-     def _get_ipa_session(self, config):  # pragma: no cover

-         """Makes an ipa session that handles logging in"""

-         if self._ipa_session is None:

-             conf_vars = ("KRB5_CONFIG", "IPA_CONFDIR")

-             for conf_var in conf_vars:

-                 if not config.get(conf_var):

-                     continue

-                 os.environ[conf_var] = config[conf_var]

-             self._ipa_session = ipalib.api

-             self._ipa_session.bootstrap(context="custom")

-             self._ipa_session.load_plugins()

-             self._ipa_session.finalize()

-             self._ipa_session.Backend.rpcclient.connect()

-         return self._ipa_session

- 

-     def process(self, config, message):

-         """Process a given message. Remove user that is being removed from

-         a packager group from all groups in distgit"""

+     def remove_user(self, config, message):

+         watched_groups = config.get("watched_groups", [])

          group = message.group

-         watched_groups = config.get("watched_groups")

- 

          # If user is not removed from packager group, bail

          if group not in watched_groups:

              _log.info(
@@ -69,12 +67,6 @@ 

              )

              return

  

-         self.dist_git = pagure.set_pagure(

-             {

-                 "pagure_url": config.get("dist_git_url"),

-                 "pagure_api_key": config.get("dist_git_token"),

-             }

-         )

          _log.info("Fetching all distgit groups:")

          distgit_groups = self.dist_git.get_all_groups()

          _log.info(f"Distgit groups found: {distgit_groups}")
@@ -83,19 +75,80 @@ 

              _log.info("No distgit groups found, bailing.")

              return

  

-         ipa_session = self._get_ipa_session(config)

- 

          for user in message.user_names:

-             self._process_removal(config, user, group, distgit_groups, ipa_session)

+             self._process_removal(config, user, group, distgit_groups)

+ 

+     def find_and_remove(self, config):

+         """Find users that are members of distgit groups but not members of IPA packager groups

+         and remove them from distgit and ipa groups, including removing the ipa sponsor membership.

+         """

+         _log.info("Starting find_and_remove process")

+ 

+         # Get all distgit groups

+         try:

+             distgit_groups = self.dist_git.get_all_groups()

+             _log.info(f"Found {len(distgit_groups)} distgit groups")

+         except PagureError as e:

+             _log.error(f"Failed to get distgit groups: {str(e)}")

+             return

+ 

+         # Get all members of IPA packager groups, including sponsors

+         watched_groups = config.get("watched_groups", [])

+         packager_members = set()

+         for group in watched_groups:

+             try:

+                 group_info = self._ipa_session.Command.group_show(cn=group)

+                 if "member_user" in group_info["result"]:

+                     packager_members.update(group_info["result"]["member_user"])

+                 if "membermanager_user" in group_info["result"]:

+                     packager_members.update(group_info["result"]["membermanager_user"])

+             except Exception as e:

+                 _log.error(f"Failed to get members of IPA group {group}: {str(e)}")

+                 continue

  

-     def _process_removal(self, config, user, group, distgit_groups, ipa_session):

          _log.info(

-             f"User {user} was removed from {group} group, removing from "

-             "packager-related groups as well."

+             f"Found {len(packager_members)} unique packagers (members and sponsors) "

+             "in IPA packager groups"

          )

+         # Process each distgit group

+         for group in distgit_groups:

+             group_members = self.dist_git.get_group_members(group)

+             if not group_members:

+                 _log.exception(f"No group members found in distgit group {group}")

+                 continue

+             _log.info(f"Processing group {group} with {len(group_members)} members")

+ 

+             # Members who are not in IPA packagers groups need to be removed

+             members_to_remove = [

+                 member for member in group_members if member not in packager_members

+             ]

+ 

+             if not members_to_remove:

+                 _log.info(f"No members to remove from group {group}")

+                 continue

  

-         # Remove user from the distgit group that triggered the toddler

+             _log.info(

+                 f"Found {len(members_to_remove)} members "

+                 f"to remove from distgit and IPA group {group}"

+             )

+             for user in members_to_remove:

+                 # Remove members and sponsors from distgit

+                 self._remove_from_distgit_and_ipa(user, group)

+ 

+         _log.info("Completed find_and_remove process")

+ 

+     def _remove_from_distgit_and_ipa(self, user, group):

+         try:

+             self._remove_from_ipa(user, group)

+         except IPAError as e:

+             _log.exception(e.message)

+         self._remove_from_distgit(user, group)

+ 

+     def _remove_from_distgit(self, user, group):

+         _log.info(f"Removing user {user} from distgit group {group}")

          try:

+             # Remove user from distgit groups:

+             # pagure token with acls=group_modify needed

              self.dist_git.remove_member_from_group(user, group)

              _log.info(f"User {user} removed from distgit group: {group}")

          except PagureError:
@@ -103,8 +156,86 @@ 

                  f"Error while removing user {user} from distgit group {group}"

              )

  

+     def _check_ipa_removal_output(self, output, user, group, role):

+         """Check IPA removal command output and raise errors if unsuccessful."""

+         if not output:

+             raise IPAError(

+                 f"Error while removing user {user} from {role} of ipa group {group}, no output"

+             )

+ 

+         if output["completed"] == 0:

+             raise IPAError(

+                 f"Removing user {user} from {role} of ipa group {group} was not "

+                 f"successful, output: {output}"

+             )

+ 

+     def _remove_from_ipa(self, user, group):

+         # Remove sponsors first, followed by members, from ipa groups

+         group_info = self._ipa_session.Command.group_show(cn=group)

+         sponsors = group_info["result"].get("membermanager_user", [])

+         members = group_info["result"].get("member_user", [])

+ 

+         error_message = f"Error while removing user {user} from ipa group {group}"

+ 

+         try:

+             # Remove from sponsors if user is a sponsor

+             if user in sponsors:

+                 _log.info(f"Removing user {user} from sponsors of ipa group {group}")

+                 output_sponsor = self._ipa_session.Command.group_remove_member_manager(

+                     cn=group, user=user

+                 )

+                 self._check_ipa_removal_output(output_sponsor, user, group, "sponsors")

+                 _log.info(f"User {user} removed from sponsors of ipa group {group}")

+             else:

+                 _log.info(

+                     f"User {user} is not a sponsor of ipa group {group}, skipping sponsor removal"

+                 )

+ 

+             # Remove from members if user is a member

+             if user in members:

+                 _log.info(f"Removing user {user} from members of ipa group {group}")

+                 output_member = self._ipa_session.Command.group_remove_member(

+                     cn=group, user=user

+                 )

+                 self._check_ipa_removal_output(output_member, user, group, "members")

+                 _log.info(f"User {user} removed from members of ipa group {group}")

+             else:

+                 _log.info(

+                     f"User {user} is not a member of ipa group {group}, skipping member removal"

+                 )

+ 

+         except Exception as e:

+             raise IPAError(f"{error_message}") from e

+ 

+     def process(self, config, message):

+         """Process a given message. Remove user that is being removed from

+         a packager group from all groups in distgit"""

+         topic = message.topic

+ 

+         self.dist_git = pagure.set_pagure(

+             {

+                 "pagure_url": config.get("dist_git_url"),

+                 "pagure_api_key": config.get("dist_git_token"),

+             }

+         )

+         with self._ipa_session:

+             if topic.endswith("toddlers.trigger.clean_packagers_groups"):

+                 self.find_and_remove(config)

+ 

+             if topic.endswith("ipa.group_remove_member.v1"):

+                 self.remove_user(config, message)

+ 

+     def _process_removal(self, config, user, group, distgit_groups):

+         _log.info(

+             f"User {user} was removed from {group} group, removing from "

+             "packager-related groups as well."

+         )

+ 

+         # Remove user from the distgit group that triggered the toddler

+         self._remove_from_distgit(user, group)

+ 

          _log.info(f"Fetching groups user {user} is member of in ipa:")

-         ipa_user = ipa_session.Command.user_show(uid=user)

+         ipa_user = self._ipa_session.Command.user_show(uid=user)

          ipa_groups = ipa_user["result"]["memberof_group"]

          _log.info(f"Ipa groups: {ipa_groups}")

  
@@ -121,20 +252,4 @@ 

          )

  

          for group in intersection:

-             try:

-                 # Remove user from ipa groups:

-                 ipa_session.Command.group_remove_member(cn=group, user=user)

-                 _log.info(f"User {user} removed from ipa group: {group}")

-             except Exception:

-                 _log.exception(

-                     f"Error while removing user {user} from ipa group {group}"

-                 )

-             try:

-                 # Remove user from distgit groups:

-                 # pagure token with acls=group_modify needed

-                 self.dist_git.remove_member_from_group(user, group)

-                 _log.info(f"User {user} removed from distgit group: {group}")

-             except PagureError:

-                 _log.exception(

-                     f"Error while removing user {user} from distgit group {group}"

-                 )

+             self._remove_from_distgit_and_ipa(user, group)

@@ -24,7 +24,14 @@ 

  

  from toddlers.base import ToddlerBase

  from toddlers.exceptions import ValidationError

- from toddlers.utils import bugzilla_system, fedora_account, git, pagure, requests

+ from toddlers.utils import (

+     anitya,

+     bugzilla_system,

+     fedora_account,

+     git,

+     pagure,

+     requests,

+ )

  

  # Regex for branch name validation

  STREAM_NAME_REGEX = r"^[a-zA-Z0-9.\-_+]+$"
@@ -100,6 +107,9 @@ 

      # for toddler

      pagure_user: str = ""

  

+     # Anitya object to work with Anitya

+     anitya: anitya.Anitya

+ 

      def accepts_topic(self, topic: str) -> bool:

          """Returns a boolean whether this toddler is interested in messages

          from this specific topic.
@@ -187,6 +197,9 @@ 

          _log.info("Setting up connection to Bugzilla")

          bugzilla_system.set_bz(config)

  

+         _log.info("Setting up connection to Anitya")

+         self.anitya = anitya.set_anitya(config)

+ 

          try:

              if message.topic.endswith("pagure.issue.comment.added"):

                  self.process_comment(issue)
@@ -450,6 +463,12 @@ 

              "namespace",

              "sls",

              "monitor",

+             "upstreamurl",

+         ]

+         required_keys_for_monitor = [

+             "backend",

+             "project_name",

+             "distribution",

          ]

          for key in required_keys:

              if key not in issue_body_json.keys():
@@ -461,6 +480,18 @@ 

                  )

                  return

  

+         monitor = issue_body_json.get("monitor", "").strip()

+         if monitor != "no-monitoring":

+             for key in required_keys_for_monitor:

+                 if key not in issue_body_json.keys():

+                     self.pagure_io.close_issue(

+                         issue["id"],

+                         namespace=PROJECT_NAMESPACE,

+                         message="Invalid body, missing required field: {}".format(key),

+                         reason="Invalid",

+                     )

+                     return

+ 

          # Validate the request first

          if self._validate_new_repo_request(issue, issue_body_json):

              _log.info("Ticket passed all validations. Creating repository.")
@@ -636,6 +667,7 @@ 

          branch_name = issue_body_json.get("branch", "").strip()

          description = issue_body_json.get("description", "").strip()

          upstreamurl = issue_body_json.get("upstreamurl", "").strip()

+         monitor = issue_body_json.get("monitor", "").strip()

  

          if namespace in ["rpms", "container"]:

              default_branch = "rawhide"
@@ -736,6 +768,50 @@ 

                  'You may commit to the branch "{1}" in about '

                  "10 minutes.".format(dist_git_url, branch_name)

              )

+ 

+         if monitor != "no-monitoring":

+             _log.info("- Checking if project {0} exists in Anitya".format(repo))

+             backend = issue_body_json["backend"].strip()

+             distribution = issue_body_json["distribution"].strip()

+             project_name = issue_body_json["project_name"].strip()

+ 

+             monitoring_message = ""

+             project_msg = ""

+             package_msg = ""

+             anitya_project_url = self.anitya.does_project_exists_in_anitya(project_name)

+             if anitya_project_url is None:

+                 anitya_project_url = self.anitya.create_project_in_anitya(

+                     repo, upstreamurl, backend

+                 )

+             if anitya_project_url is None:

+                 project_msg = (

+                     "Wasn't able to create project in Anitya. "

+                     "You can create it manually on: `https://release-monitoring.org`"

+                 )

+             else:

+                 project_msg = (

+                     "Anitya project is accessible by this link \n`{0}`\n "

+                     "you can modify it manually."

+                 ).format(anitya_project_url)

+                 package_exists = self.anitya.does_package_exists_in_anitya(

+                     repo, project_name, distribution

+                 )

+                 if not package_exists:

+                     response_msg = self.anitya.create_package_in_anitya(

+                         repo, project_name, distribution, upstreamurl

+                     )

+                     if response_msg != "Success":

+                         package_msg = (

+                             "Package wasn't created in Anitya, reason: `{0}`.".format(

+                                 response_msg

+                             )

+                         )

+                     else:

+                         package_msg = "Package was created in Anitya"

+ 

+             monitoring_message = project_msg + "\n" + package_msg

+             new_repo_comment = new_repo_comment + "\nMonitoring:\n" + monitoring_message

+ 

          self.pagure_io.close_issue(

              issue["id"],

              namespace=PROJECT_NAMESPACE,

@@ -0,0 +1,581 @@ 

+ """

+ This is a script to automate unretirement of package automatically, when ticket is created.

+ 

+ Authors:    Anton Medvedev <amedvede@redhat.com>

+ 

+ """

+ 

+ import argparse

+ import json

+ import logging

+ import re

+ import sys

+ import tempfile

+ import traceback

+ from typing import Optional

+ 

+ import arrow

+ from fedora_messaging.api import Message

+ from git import GitCommandError

+ import koji

+ from pagure_messages.issue_schema import IssueNewV1

+ import tomllib

+ 

+ from toddlers.base import ToddlerBase

+ from toddlers.exceptions import ValidationError

+ from toddlers.utils import bodhi, bugzilla_system, git, pagure, requests

+ 

+ 

+ # Where to look for unretire request tickets

+ PROJECT_NAMESPACE = "releng/fedora-scm-requests"

+ # Keyword that will be searched for in the issue title

+ UNRETIRE_KEYWORD = "unretire"

+ # RPM package prefix, that will be searched in the issue title

+ RPM_PREFIX = "rpms/"

+ # Forbidden keywords for commit message

+ FORBIDDEN_KEYWORDS_FOR_COMMIT_MESSAGE = ["legal", "license"]

+ # Time difference limit not getting Bugzilla url

+ TIME_DIFFERENCE_LIMIT = 56  # 8 weeks in days

+ # Package retirement process url

+ PACKAGE_RETIREMENT_PROCESS_URL = (

+     "https://docs.fedoraproject.org/en-US/package-maintainers"

+     "/Package_Retirement_Process/#claiming"

+ )

+ # Fedora review bugzilla flag

+ FEDORA_REVIEW_FLAG_NAME = "fedora-review"

+ # Koji hub url

+ KOJIHUB_URL = "https://koji.fedoraproject.org/kojihub"

+ 

+ _log = logging.getLogger(__name__)

+ 

+ 

+ class UnretirePackages(ToddlerBase):

+     """

+     Listen for new tickets in https://pagure.io/releng/fedora-scm-requests/issues

+     and process then, either by unretiring a package or rejecting the ticket

+     """

+ 

+     name: str = "unretire_packages"

+ 

+     amqp_topics: list = ["io.pagure.*.pagure.issue.new"]

+ 

+     # Path to temporary dir

+     temp_dir: str = ""

+ 

+     # Requests session

+     requests_session: requests.requests.Session

+ 

+     # Dist-git base url

+     dist_git_base: Optional[str] = ""

+ 

+     # Pagure object connected to pagure.io

+     pagure_io: pagure.Pagure

+ 

+     # Pagure user that will be creating the comments on pagure

+     # for toddler

+     pagure_user: str = ""

+ 

+     # Git repo object

+     git_repo: git.GitRepo

+ 

+     # Koji session object

+     koji_session: koji.ClientSession

+ 

+     # Bodhi object

+     bodhi: bodhi.Bodhi

+ 

+     # OIDC distgit token

+     oidc_distgit_token: str

+ 

+     def accepts_topic(self, topic: str) -> bool:

+         """

+         Returns a boolean whether this toddler is interested in messages

+         from this specific topic.

+ 

+         :arg topic: Topic to check

+ 

+         :returns: True if topic is accepted, False otherwise

+         """

+         if topic.startswith("io.pagure."):

+             if topic.endswith("pagure.issue.new"):

+                 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

+         """

+         _log.debug(

+             "Processing message:\n{0}".format(json.dumps(message.body, indent=2))

+         )

+         project_name = message.body["project"]["fullname"]

+ 

+         if project_name != PROJECT_NAMESPACE:

+             _log.info(

+                 "The message doesn't belong to project {0}. Skipping message.".format(

+                     PROJECT_NAMESPACE

+                 )

+             )

+             return

+ 

+         issue = message.body["issue"]

+ 

+         if issue["status"] != "Open":

+             _log.info(

+                 "The issue {0} is not open. Skipping message.".format(issue["id"])

+             )

+             return

+ 

+         issue_title = issue["title"]

+         words_in_issue_title = issue_title.split()

+         if UNRETIRE_KEYWORD != words_in_issue_title[0].lower():

+             _log.info(

+                 "The issue doesn't contain keyword '{0}' in the title '{1}'"

+                 "".format(UNRETIRE_KEYWORD, issue_title)

+             )

+             return

+ 

+         _log.debug("Getting temp_folder name from config.")

+         self.temp_dir = config.get("temp_folder", "")

+ 

+         _log.debug("Creating a request session.")

+         self.requests_session = requests.make_session()

+ 

+         _log.debug("Getting dist-git url from config.")

+         self.dist_git_base = config.get("dist_git_url")

+ 

+         _log.debug("Setting up connection to Pagure")

+         self.pagure_io = pagure.set_pagure(config)

+         self.pagure_user = config.get("pagure_user", "")

+ 

+         _log.debug("Setting up connection to Bugzilla")

+         bugzilla_system.set_bz(config)

+ 

+         _log.debug("Setting up session with Koji")

+         self.koji_session = koji.ClientSession(KOJIHUB_URL)

+ 

+         _log.debug("Setting up bodhi session")

+         self.bodhi = bodhi.set_bodhi(config)

+ 

+         _log.debug("Getting OIDC distgit token from config.")

+         self.oidc_distgit_token = config.get("oidc_distgit_token", "")

+ 

+         try:

+             self.process_ticket(issue)

+         except BaseException:

+             self.pagure_io.add_comment_to_issue(

+                 issue["id"],

+                 namespace=PROJECT_NAMESPACE,

+                 comment=(

+                     "Error happened during processing:\n" "```\n" "{0}\n" "```\n"

+                 ).format(traceback.format_exc()),

+             )

+ 

+     def process_ticket(self, issue: dict) -> None:

+         """

+         Process a single ticket

+ 

+         :arg issue: A dictionary containing the issue

+         """

+         _log.info("Handling pagure releng ticket '{0}'".format(issue["full_url"]))

+         try:

+             # If a ValueError is raised, that means it isn't valid JSON

+             issue_body = json.loads(issue["content"].strip("`").strip("\n"))

+         except ValueError:

+             _log.info("Invalid JSON in ticket. Closing '{0}'".format(issue["full_url"]))

+             self.pagure_io.close_issue(

+                 issue["id"],

+                 namespace=PROJECT_NAMESPACE,

+                 message="Invalid JSON provided",

+                 reason="Invalid",

+             )

+             return

+ 

+         package_name = issue_body["name"]

+         package_ns = issue_body["type"]

+         maintainer_fas = issue_body["maintainer"]

+ 

+         package_ns = self._ns_convertor(package_ns)

+ 

+         package_url = "{0}/{1}/{2}.git".format(

+             self.dist_git_base, package_ns, package_name

+         )

+ 

+         _log.debug("Verifying that package repository actually exist.")

+         if not self._does_url_exist(package_url):

+             msg = "Package repository doesnt exist. Try to repeat request."

+             _log.info(msg)

+             self.pagure_io.close_issue(

+                 issue["id"],

+                 namespace=PROJECT_NAMESPACE,

+                 message=msg,

+                 reason="Invalid",

+             )

+             return

+ 

+         _log.debug("Creating temporary directory")

+         with tempfile.TemporaryDirectory(dir=self.temp_dir) as tmp_dir:

+             _log.info("Cloning repo into dir with name '{0}'".format(self.temp_dir))

+             try:

+                 self.git_repo = git.clone_repo(package_url, tmp_dir)

+             except GitCommandError:

+                 message = "Something went wrong during cloning git repository."

+                 _log.info(message)

+                 self.pagure_io.close_issue(

+                     issue["id"],

+                     namespace=PROJECT_NAMESPACE,

+                     message=message,

+                     reason="Invalid",

+                 )

+                 return

+ 

+             branches = issue_body["branches"]

+ 

+             _log.debug("Getting active branches")

+             active_branches = self.bodhi.get_active_branches()

+ 

+             filtered_branches = [

+                 branch for branch in branches if branch in active_branches

+             ]

+ 

+             final_list_of_branches = []

+             deadpackage_file_path = "dead.package"

+             _log.debug("Verifying that branches are actually exists.")

+             _log.debug(

+                 "Verifying that branches are actually retired (have a `dead.package` file)."

+             )

+             for branch in filtered_branches:

+                 if self.git_repo.does_branch_exist(branch):

+                     if self.git_repo.does_branch_contains_file(

+                         branch, deadpackage_file_path

+                     ):

+                         final_list_of_branches.append(branch)

+ 

+             _log.debug("Verifying if package is ready for unretirement.")

+             if not self._is_package_ready_for_unretirement(

+                 issue_id=issue["id"],

+                 branches=final_list_of_branches,

+                 review_bugzilla=issue_body["review_bugzilla"],

+             ):

+                 return

+ 

+             _log.debug("Reverting retire commit")

+             revert_commit_message = "Unretirement request: {0}".format(

+                 issue["full_url"]

+             )

+             for branch in final_list_of_branches:

+                 self.git_repo.revert_last_commit(

+                     message=revert_commit_message,

+                     user=self.pagure_user,

+                     token=self.oidc_distgit_token,

+                     branch=branch,

+                 )

+ 

+             _log.debug("Unblocking tags on Koji.")

+             if self._check_tags_to_unblock(final_list_of_branches, package_name):

+                 _log.debug("Unblocking tags in koji.")

+                 for tag in final_list_of_branches:

+                     try:

+                         self.koji_session.packageListUnblock(

+                             taginfo=tag, pkginfo=package_name

+                         )

+                     except koji.GenericError:

+                         msg = "Not able to unblock `{0}` tag on koji.".format(tag)

+                         self.pagure_io.close_issue(

+                             issue_id=issue["id"],

+                             namespace=PROJECT_NAMESPACE,

+                             message=msg,

+                             reason="Invalid",

+                         )

+                         return

+ 

+             _log.debug("Verifying package is not orphan.")

+             if self.pagure_io.is_project_orphaned(

+                 namespace=package_ns, repo=package_name

+             ):

+                 if maintainer_fas == "":

+                     msg = "Package is ophaned, but maintainer fas is not provided."

+                     self.pagure_io.close_issue(

+                         issue_id=issue["id"],

+                         namespace=PROJECT_NAMESPACE,

+                         message=msg,

+                         reason="Invalid",

+                     )

+                     return

+                 self.pagure_io.assign_maintainer_to_project(

+                     namespace=package_ns,

+                     repo=package_name,

+                     maintainer_fas=maintainer_fas,

+                 )

+ 

+         _log.info(

+             "Package {0} is assigned to {1}".format(

+                 f"{package_ns}/{package_name}", maintainer_fas

+             )

+         )

+         return

+ 

+     def _is_package_ready_for_unretirement(

+         self, issue_id: int, branches: list, review_bugzilla: str

+     ) -> bool:

+         """

+         Verify that package is ready for unretirement.

+ 

+         :arg issue_id: An int value of issue ID.

+         :arg branches: A list containing branches that need to be unretired.

+         :arg review_bugzilla: A str contain url on bugzilla review.

+ 

+         :returns: Bool value whether the package was verified.

+         """

+         try:

+             _log.debug("Verifying the reason of retirement.")

+             self._verify_package_not_retired_for_reason(branches=branches)

+             _log.debug("Verifying the date of retirement.")

+             self._verify_bugzilla_ticket(

+                 review_bugzilla=review_bugzilla, branches=branches

+             )

+         except ValidationError as error:

+             self.pagure_io.close_issue(

+                 issue_id=issue_id,

+                 namespace=PROJECT_NAMESPACE,

+                 message=str(error),

+                 reason="Invalid",

+             )

+             return False

+         return True

+ 

+     def _verify_package_not_retired_for_reason(self, branches: list):

+         """

+         Verify that commit message does not contain forbidden keywords.

+ 

+         Raises:

+             `toddler.exceptions.ValidationError`: When retirement reason wasn't verified

+         """

+         _log.debug("Verifying that issue message doesn't contain forbidden keywords")

+ 

+         for branch in branches:

+             last_commit_message = self.git_repo.get_last_commit_message(branch)

+             if any(

+                 re.search(forbidden_keyword, str(last_commit_message).lower())

+                 for forbidden_keyword in FORBIDDEN_KEYWORDS_FOR_COMMIT_MESSAGE

+             ):

+                 raise ValidationError(

+                     "Package was retired for a reason: legal or license issue."

+                 )

+ 

+     def _verify_bugzilla_ticket(self, review_bugzilla, branches):

+         """

+         Verify if last commit was made more than 8 weeks ago, need to request a bugzilla ticket.

+         """

+         _log.debug("Verifying that retire commit was made less than 8 weeks ago.")

+ 

+         is_need_to_verify_bz = False

+ 

+         for branch in branches:

+             last_commit_date = self.git_repo.get_last_commit_date(branch)

+             if last_commit_date is None:

+                 raise ValidationError("Couldn't get a date of the retire commit.")

+             else:

+                 last_commit_date = arrow.get(last_commit_date)

+ 

+             current_time = arrow.utcnow()

+ 

+             time_diff_in_days = (current_time - last_commit_date).days

+ 

+             if time_diff_in_days > TIME_DIFFERENCE_LIMIT:

+                 is_need_to_verify_bz = True

+ 

+         if not is_need_to_verify_bz:

+             return

+ 

+         if review_bugzilla == "":

+             raise ValidationError(

+                 "Bugzilla url is missing, please add it and recreate the ticket."

+             )

+ 

+         bug_id = review_bugzilla

+ 

+         _log.debug("Getting the bug object from bugzilla.")

+         try:

+             bug = bugzilla_system.get_bug(bug_id)

+         except Exception as error:

+             raise ValidationError(

+                 "The Bugzilla bug could not be verified. The following "

+                 "error was encountered: {0}".format(str(error))

+             )

+ 

+         if bug is None:

+             raise ValidationError(

+                 "Bugzilla can't get the bug by bug id, fix bugzilla url."

+             )

+ 

+         if bug.product != "Fedora":

+             raise ValidationError(

+                 "The bugzilla bug is for '{0}', "

+                 "but request should be for 'Fedora'.".format(bug.product)

+             )

+ 

+         try:

+             _log.info("Getting {0} flag from bug".format(FEDORA_REVIEW_FLAG_NAME))

+             fedora_review_flag = bug.get_flags(FEDORA_REVIEW_FLAG_NAME)

+             fedora_review_flag_status = fedora_review_flag[0]["status"]

+ 

+             if fedora_review_flag_status != "+":

+                 raise ValidationError(

+                     "Flag fedora-review has wrong status, need to be +"

+                 )

+         except TypeError:

+             raise ValidationError(

+                 "Tag fedora-review is missing on bugzilla, get it and recreate the ticket."

+             )

+ 

+     def _check_tags_to_unblock(self, tags_to_unblock: list, repo: str) -> bool:

+         """

+         Check if at least one of the tags requested to be unblocked are really blocked.

+ 

+         :arg tags_to_unblock: List of branch names

+         :arg repo: Name of package

+ 

+         :returns: Bool value whether program need to unblock tags

+         """

+         _log.debug("Verifying that tags are blocked on koji.")

+         try:

+             package_tags = self.koji_session.listTags(package=repo)

+             if not package_tags:

+                 raise ValidationError("Package doesn't have tags on koji.")

+             tags_that_suppose_to_be_blocked = []

+ 

+             for tag in package_tags:

+                 prefix = "dist-"

+                 if tag["name"].startswith(prefix):

+                     tag_name = tag["name"][len(prefix) :]  # noqa: E203

+                     if tag_name in tags_to_unblock:

+                         tags_that_suppose_to_be_blocked.append(tag)

+ 

+             if len(tags_that_suppose_to_be_blocked) == 0:

+                 raise ValidationError(

+                     "Request to unblock tags that don't exist on koji."

+                 )

+             return any([tag["locked"] for tag in tags_that_suppose_to_be_blocked])

+         except koji.GenericError:

+             raise ValidationError("Package doesn't exist on koji.")

+ 

+     def _does_url_exist(self, url: str) -> bool:

+         """

+         Check whether url exist.

+ 

+         :arg url: Url that might exist

+ 

+         :returns: True if url exist, otherwise False

+         """

+         try:

+             response = self.requests_session.get(url)

+         except ConnectionError:

+             return False

+         return response.status_code == 200

+ 

+     @staticmethod

+     def _ns_convertor(namespace):

+         ns_mapping = {

+             "rpm": "rpms",

+             "test": "tests",

+             "flatpak": "flatpaks",

+             "module": "modules",

+         }

+         namespace = ns_mapping[namespace] if namespace in ns_mapping else namespace

+         return namespace

+ 

+ 

+ def _get_arguments(args):

+     """Load and parse the CLI arguments.

+ 

+     :arg args: Script arguments

+ 

+     :returns: Parsed arguments

+     """

+     parser = argparse.ArgumentParser(

+         description="Processor for Unretire packages, handling tickets from '{}'".format(

+             PROJECT_NAMESPACE

+         )

+     )

+ 

+     parser.add_argument(

+         "ticket",

+         type=int,

+         help="Number of ticket to process",

+     )

+ 

+     parser.add_argument(

+         "--config",

+         help="Configuration file",

+     )

+ 

+     parser.add_argument(

+         "--debug",

+         action="store_const",

+         dest="log_level",

+         const=logging.DEBUG,

+         default=logging.INFO,

+         help="Enable debugging output",

+     )

+     return parser.parse_args(args)

+ 

+ 

+ def _setup_logging(log_level: int) -> None:

+     """

+     Set up the logging level.

+ 

+     :arg log_level: Log level to set

+     """

+     handlers = []

+ 

+     _log.setLevel(log_level)

+     # We want all messages logged at level INFO or lower to be printed to stdout

+     info_handler = logging.StreamHandler(stream=sys.stdout)

+     handlers.append(info_handler)

+ 

+     if log_level == logging.INFO:

+         # In normal operation, don't decorate messages

+         for handler in handlers:

+             handler.setFormatter(logging.Formatter("%(message)s"))

+ 

+     logging.basicConfig(level=log_level, handlers=handlers)

+ 

+ 

+ def main(args):

+     """Main function"""

+     args = _get_arguments(args)

+     _setup_logging(log_level=args.log_level)

+     _log.info("hello i'm starting work")

+ 

+     config = tomllib.load(args.config)

+ 

+     ticket = args.ticket

+ 

+     pagure_io = pagure.set_pagure(config)

+     issue = pagure_io.get_issue(ticket, PROJECT_NAMESPACE)

+ 

+     # Convert issue to message

+     body = {"project": {"fullname": PROJECT_NAMESPACE}, "issue": issue}

+     message = IssueNewV1(body=body)

+     _log.debug("Message prepared: {}".format(message.body))

+ 

+     UnretirePackages().process(

+         config=config,

+         message=message,

+     )

+ 

+ 

+ if __name__ == "__main__":  # pragma: no cover

+     try:

+         main(sys.argv[1:])

+     except KeyboardInterrupt:

+         pass

@@ -0,0 +1,212 @@ 

+ """

+ This module is a wrapper for Anitya. It uses Anitya API to communicate and configure

+ release monitoring instance.

+ To work with it, you need to set it up by calling `set_anitya`.

+ 

+ Examples:

+     from utils import anitya

+ 

+     anitya_config = {

+         "anitya_endpoint": "https://release-monitoring.org/",

+         "anitya_access_token": "secret TOKEN",

+     }

+ 

+     anitya_obj = anitya.set_anitya(config)

+     anitya_obj.create_project_in_anitya("<name>", "<homepage>", "<backend>")

+ """

+ 

+ import logging

+ from typing import Optional

+ 

+ from toddlers.utils import requests

+ 

+ log = logging.getLogger(__name__)

+ 

+ 

+ def set_anitya(config):

+     """

+     Set the connection to the Anitya API.

+ 

+     Params:

+         config: Configuration dictionary.

+     """

+     return Anitya(config)

+ 

+ 

+ class Anitya(object):

+     """

+     Object that works with Anitya.

+     """

+ 

+     # URL to Anitya

+     _anitya_endpoint: str = ""

+     # API TOKEN to Anitya

+     _anitya_access_token: Optional[str] = None

+     # Request Session object used for communication

+     _requests_session: requests.requests.Session

+ 

+     def __init__(self, config):

+         """

+         Initialize the Anitya class.

+ 

+         Params:

+             config (dict): A configuration with anitya_endpoint and anitya_access_token keys.

+ 

+         Raises:

+             ValueError: If no pagure_api_key is provided.

+         """

+         self._anitya_endpoint = config.get("anitya_endpoint", "").removesuffix("/")

+         if not self._anitya_endpoint:

+             raise ValueError("No anitya endpoint found in config file")

+ 

+         self._anitya_token = config.get("anitya_access_token", "")

+         if not self._anitya_token:

+             raise ValueError("No anitya access token found in config file")

+ 

+         self._requests_session = requests.make_session(timeout=300)

+ 

+     def does_project_exists_in_anitya(self, project_name: str) -> Optional[str]:

+         """

+         Check if project exists in Anitya.

+ 

+         Params:

+             project_name (str): The name of the project.

+ 

+         Returns:

+             Optional[str]: project URL if it exists in Anitya, otherwise None.

+         """

+         projects_params = {

+             "name": project_name,

+         }

+         endpoint = self._anitya_endpoint + "/api/v2/projects/"

+         projects_response = self._requests_session.get(endpoint, params=projects_params)

+         if projects_response.status_code != 200:

+             log.debug("Project '{0}' not found in Anitya.".format(project_name))

+             return None

+ 

+         response_json = projects_response.json()

+         item_count = response_json["total_items"]

+         if item_count == 0:

+             log.debug("Project '{0}' not found in Anitya.".format(project_name))

+             return None

+ 

+         try:

+             project_id = response_json["items"][0]["id"]

+             project_url = "{0}/project/{1}".format(self._anitya_endpoint, project_id)

+             return project_url

+         except (KeyError, IndexError):

+             return None

+ 

+     def does_package_exists_in_anitya(

+         self, package_name: str, distribution: str, project_name: str

+     ) -> bool:

+         """

+         Check if package exists in Anitya.

+ 

+         Params:

+             package_name (str): The name of the package.

+             distribution (str): The name of the distribution.

+             project_name (str): The name of the project.

+ 

+         Returns:

+             False if package don't exist

+             False if package exist but his project is different from provided project name

+             True if package exist and his project is correct

+         """

+         endpoint = self._anitya_endpoint + "/api/v2/packages/"

+         packages_params = {

+             "name": package_name,

+             "distribution": distribution,

+         }

+         packages_response = self._requests_session.get(endpoint, params=packages_params)

+         if packages_response.status_code != 200:

+             log.info("Package '{0}' not found in Anitya.".format(package_name))

+             return False  # Not able to find package

+         response_json = packages_response.json()

+         item_count = response_json["total_items"]

+         if item_count < 1:

+             log.info("Package '{0}' not found in Anitya.".format(package_name))

+             return False

+         package_json = response_json["items"][0]

+         package_project_name = package_json["project"]

+         if package_project_name != project_name:

+             return False  # Expected and actual project name are different

+         else:

+             return True  # Expected and actual project name are the same

+ 

+     def create_project_in_anitya(

+         self,

+         name: str,

+         homepage: str,

+         backend: str,

+     ) -> Optional[str]:

+         """

+         Create a new project in Anitya.

+ 

+         Params:

+             name (str): The name of the project.

+             homepage (str): The homepage of the project.

+             backend (str): The name of the backend.

+ 

+         Returns:

+             The project URL if successful, otherwise None.

+         """

+         headers = {"Authorization": "token " + self._anitya_token}

+         endpoint = self._anitya_endpoint + "/api/v2/projects/"

+         payload = {

+             "name": name,

+             "homepage": homepage,

+             "backend": backend,

+         }

+         log.info("Creating project '{0}' in Anitya.".format(name))

+         response = self._requests_session.post(

+             url=endpoint, data=payload, headers=headers

+         )

+         if response.status_code == 201:

+             project_json = response.json()

+             project_id = project_json["id"]

+             project_url = "{0}/project/{1}".format(self._anitya_endpoint, project_id)

+             return project_url

+         else:

+             return None

+ 

+     def create_package_in_anitya(

+         self,

+         package_name: str,

+         project_name: str,

+         distribution: str,

+         project_ecosystem: str,

+     ) -> Optional[str]:

+         """

+         Create a new package in Anitya.

+ 

+         Params:

+             package_name (str): The name of the package.

+             project_name (str): The name of the project.

+             distribution (str): The name of the distribution.

+             project_ecosystem (str): The name of the ecosystem.

+ 

+         Returns:

+             Return message if status code is known, otherwise None.

+         """

+         headers = {"Authorization": "token " + self._anitya_token}

+         endpoint = self._anitya_endpoint + "/api/v2/packages/"

+         payload = {

+             "package_name": package_name,

+             "project_name": project_name,

+             "distribution": distribution,

+             "project_ecosystem": project_ecosystem,

+         }

+         log.info("Creating package '{0}' in Anitya.".format(package_name))

+         response = self._requests_session.post(

+             url=endpoint, data=payload, headers=headers

+         )

+         if response.status_code == 400:

+             return "Bad Request, some necessary arguments were not provided."

+         elif response.status_code == 401:

+             return "Unauthorized, access token is incorrect."

+         elif response.status_code == 409:

+             return "Conflict, package already exists."

+         elif response.status_code == 201:

+             return "Success"

+         return None

file modified
+12 -4
@@ -136,7 +136,9 @@ 

          except Exception:  # Raised when branch name is not correct

              return None

  

-     def revert_last_commit(self, message: str, branch: str = "rawhide") -> None:

+     def revert_last_commit(

+         self, message: str, user: str, token: str, branch: str = "rawhide"

+     ) -> None:

          """

          Revert last commit with message on requested branch.

  
@@ -149,10 +151,16 @@ 

              self.repo.git.checkout(branch)

  

              # reverting last commit and changing the commit message

-             self.repo.git.execute(["git", "revert", "--no-edit", "HEAD"])

-             self.repo.git.execute(["git", "commit", "--amend", "-m", message])

+             self.repo.git.revert("HEAD", no_edit=True)

+             self.repo.git.commit("--amend", "-m", message)

+ 

              origin = self.repo.remote("origin")

-             origin.push()

+             git_cmd = self.repo.git

+ 

+             push_url = origin.url.replace(

+                 "https://", "https://{0}:{1}@".format(user, token)

+             )

+             git_cmd.push("-u", push_url, branch)

          except Exception as error:

              print(error, "\nSomething happened during reverting the last commit")

  

file modified
+2 -1
@@ -1043,7 +1043,8 @@ 

                  )

              )

  

-         log.debug("User '%s' removed from group '%s'", user, group)

+         else:

+             log.info("User '%s' removed from group '%s'", user, group)

  

      def get_group_members(self, group: str) -> List[str]:

          """