From 378d3b697050869882f086aecb65c682f30264c9 Mon Sep 17 00:00:00 2001
From: Matej Focko <mfocko@redhat.com>
Date: Jul 18 2022 09:09:12 +0000
Subject: Add API endpoint for reopening pull requests


Signed-off-by: Matej Focko <mfocko@redhat.com>

---

diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py
index eef1a10..f1580e7 100644
--- a/pagure/api/__init__.py
+++ b/pagure/api/__init__.py
@@ -589,6 +589,7 @@ def api():
         fork.api_pull_request_merge,
         fork.api_pull_request_rebase,
         fork.api_pull_request_close,
+        fork.api_pull_request_reopen,
         fork.api_pull_request_add_comment,
         fork.api_pull_request_add_flag,
         fork.api_pull_request_get_flag,
diff --git a/pagure/api/fork.py b/pagure/api/fork.py
index 547e93b..5312c57 100644
--- a/pagure/api/fork.py
+++ b/pagure/api/fork.py
@@ -797,6 +797,74 @@ def api_pull_request_close(repo, requestid, username=None, namespace=None):
     return jsonout
 
 
+@API.route("/<repo>/pull-request/<int:requestid>/reopen", methods=["POST"])
+@API.route(
+    "/<namespace>/<repo>/pull-request/<int:requestid>/reopen", methods=["POST"]
+)
+@API.route(
+    "/fork/<username>/<repo>/pull-request/<int:requestid>/reopen",
+    methods=["POST"],
+)
+@API.route(
+    "/fork/<username>/<namespace>/<repo>/pull-request/<int:requestid>/reopen",
+    methods=["POST"],
+)
+@api_login_required(acls=["pull_request_close", "pull_request_update"])
+@api_method
+def api_pull_request_reopen(repo, requestid, username=None, namespace=None):
+    """
+    Reopen a pull-request
+    --------------------
+    Instruct Pagure to reopen a pull request.
+
+    ::
+
+        POST /api/0/<repo>/pull-request/<request id>/reopen
+        POST /api/0/<namespace>/<repo>/pull-request/<request id>/reopen
+
+    ::
+
+        POST /api/0/fork/<username>/<repo>/pull-request/<request id>/reopen
+        POST /api/0/fork/<username>/<namespace>/<repo>/pull-request/<request id>/reopen
+
+    Sample response
+    ^^^^^^^^^^^^^^^
+
+    ::
+
+        {
+          "message": "Pull-request reopened!"
+        }
+
+    """  # noqa
+    output = {}
+
+    repo = _get_repo(repo, username, namespace)
+    _check_pull_request(repo)
+    _check_token(repo, project_token=False)
+    request = _get_request(repo, requestid)
+
+    if (
+        not is_repo_committer(repo)
+        and not flask.g.fas_user.username == request.user.username
+    ):
+        raise pagure.exceptions.APIError(403, error_code=APIERROR.ENOPRCLOSE)
+
+    try:
+        pagure.lib.query.reopen_pull_request(
+            flask.g.session, request, flask.g.fas_user.username
+        )
+        flask.g.session.commit()
+        output["message"] = "Pull-request reopened!"
+    except SQLAlchemyError as err:  # pragma: no cover
+        flask.g.session.rollback()
+        _log.exception(err)
+        raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
+
+    jsonout = flask.jsonify(output)
+    return jsonout
+
+
 @API.route("/<repo>/pull-request/<int:requestid>/comment", methods=["POST"])
 @API.route(
     "/<namespace>/<repo>/pull-request/<int:requestid>/comment",
diff --git a/tests/test_pagure_flask_api_fork.py b/tests/test_pagure_flask_api_fork.py
index 232a384..456942d 100644
--- a/tests/test_pagure_flask_api_fork.py
+++ b/tests/test_pagure_flask_api_fork.py
@@ -1284,6 +1284,123 @@ class PagureFlaskApiForktests(tests.Modeltests):
         self.assertDictEqual(data, {"message": "Pull-request closed!"})
 
     @patch("pagure.lib.notify.send_email")
+    def test_api_pull_request_reopen(self, send_email):
+        """Test the api_pull_request_reopen method of the flask api."""
+        send_email.return_value = True
+
+        tests.create_projects(self.session)
+        tests.create_tokens(self.session)
+        tests.create_tokens_acl(self.session)
+
+        # Create the pull-request to close and reopen
+        repo = pagure.lib.query.get_authorized_project(self.session, "test")
+        forked_repo = pagure.lib.query.get_authorized_project(
+            self.session, "test"
+        )
+        req = pagure.lib.query.new_pull_request(
+            session=self.session,
+            repo_from=forked_repo,
+            branch_from="master",
+            repo_to=repo,
+            branch_to="master",
+            title="test pull-request",
+            user="pingou",
+        )
+        self.session.commit()
+        self.assertEqual(req.id, 1)
+        self.assertEqual(req.title, "test pull-request")
+
+        headers = {"Authorization": "token aaabbbcccddd"}
+
+        # Invalid project
+        output = self.app.post(
+            "/api/0/foo/pull-request/1/close", headers=headers
+        )
+        self.assertEqual(output.status_code, 404)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertDictEqual(
+            data, {"error": "Project not found", "error_code": "ENOPROJECT"}
+        )
+
+        # Valid token, wrong project
+        output = self.app.post(
+            "/api/0/test2/pull-request/1/close", headers=headers
+        )
+        self.assertEqual(output.status_code, 401)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            pagure.api.APIERROR.EINVALIDTOK.name, data["error_code"]
+        )
+        self.assertEqual(pagure.api.APIERROR.EINVALIDTOK.value, data["error"])
+
+        # Invalid PR
+        output = self.app.post(
+            "/api/0/test/pull-request/2/close", headers=headers
+        )
+        self.assertEqual(output.status_code, 404)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertDictEqual(
+            data, {"error": "Pull-Request not found", "error_code": "ENOREQ"}
+        )
+
+        # Create a token for foo for this project
+        item = pagure.lib.model.Token(
+            id="foobar_token",
+            user_id=2,
+            project_id=1,
+            expiration=datetime.datetime.utcnow()
+            + datetime.timedelta(days=30),
+        )
+        self.session.add(item)
+        self.session.commit()
+
+        # Allow the token to close and reopen PR
+        acls = pagure.lib.query.get_acls(self.session)
+        for acl in acls:
+            if acl.name == "pull_request_close":
+                break
+        item = pagure.lib.model.TokenAcl(
+            token_id="foobar_token", acl_id=acl.id
+        )
+        self.session.add(item)
+        self.session.commit()
+
+        headers = {"Authorization": "token foobar_token"}
+
+        # User not admin
+        output = self.app.post(
+            "/api/0/test/pull-request/1/close", headers=headers
+        )
+        self.assertEqual(output.status_code, 403)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertDictEqual(
+            data,
+            {
+                "error": "You are not allowed to merge/close pull-request "
+                "for this project",
+                "error_code": "ENOPRCLOSE",
+            },
+        )
+
+        headers = {"Authorization": "token aaabbbcccddd"}
+
+        # Close PR
+        output = self.app.post(
+            "/api/0/test/pull-request/1/close", headers=headers
+        )
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertDictEqual(data, {"message": "Pull-request closed!"})
+
+        # Reopen PR
+        output = self.app.post(
+            "/api/0/test/pull-request/1/reopen", headers=headers
+        )
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertDictEqual(data, {"message": "Pull-request reopened!"})
+
+    @patch("pagure.lib.notify.send_email")
     def test_api_pull_request_merge_pr_disabled(self, send_email):
         """Test the api_pull_request_merge method of the flask api when PR
         are disabled."""

