Browse Source

Add API for plugins

This commits adds four new API calls to work with plugins:
- install plugin on project
- remove plugin from project
- view enabled plugins for project
- view all available plugins in pagure

Signed-off-by: Michal Konečný <mkonecny@redhat.com>
Michal Konečný 4 years ago
parent
commit
5543e47835

+ 16 - 0
pagure/api/__init__.py

@@ -123,6 +123,10 @@ class APIERROR(enum.Enum):
     ENOPRSTATS = "No statistics could be computed for this PR"
     EUBLOCKED = "You have been blocked from this project"
     EREBASENOTALLOWED = "You are not authorized to rebase this pull-request"
+    ENOPLUGIN = "No such plugin"
+    EPLUGINDISABLED = "Plugin disabled"
+    EPLUGINCHANGENOTALLOWED = "This plugin cannot be changed"
+    EPLUGINNOTINSTALLED = "Project doesn't have this plugin installed"
 
 
 def get_authorized_api_project(session, repo, user=None, namespace=None):
@@ -304,6 +308,7 @@ from pagure.api import fork  # noqa: E402
 from pagure.api import project  # noqa: E402
 from pagure.api import user  # noqa: E402
 from pagure.api import group  # noqa: E402
+from pagure.api import plugins  # noqa: E402
 
 if pagure_config.get("PAGURE_CI_SERVICES", False):
     from pagure.api.ci import jenkins  # noqa: E402
@@ -623,6 +628,11 @@ def api():
     api_view_group_doc = load_doc(group.api_view_group)
     api_groups_doc = load_doc(group.api_groups)
 
+    api_install_plugin_doc = load_doc(plugins.api_install_plugin)
+    api_remove_plugin_doc = load_doc(plugins.api_remove_plugin)
+    api_view_plugins_project_doc = load_doc(plugins.api_view_plugins_project)
+    api_view_plugins_doc = load_doc(plugins.api_view_plugins)
+
     if pagure_config.get("ENABLE_TICKETS", True):
         api_project_tags_doc = load_doc(api_project_tags)
     api_error_codes_doc = load_doc(api_error_codes)
@@ -680,6 +690,12 @@ def api():
             api_view_user_requests_actionable_doc,
         ],
         groups=[api_groups_doc, api_view_group_doc],
+        plugins=[
+            api_install_plugin_doc,
+            api_remove_plugin_doc,
+            api_view_plugins_project_doc,
+            api_view_plugins_doc,
+        ],
         ci=ci_doc,
         extras=extras,
     )

+ 367 - 0
pagure/api/plugins.py

@@ -0,0 +1,367 @@
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2019 - Copyright Red Hat Inc
+
+ Authors:
+   Michal Konecny <mkonecny@redhat.com>
+
+"""
+
+from __future__ import print_function, unicode_literals, absolute_import
+
+import flask
+import logging
+
+from sqlalchemy.exc import SQLAlchemyError
+
+import pagure.exceptions
+import pagure.lib.query
+
+import pagure.lib.plugins as plugins_lib
+from pagure.api import (
+    API,
+    api_method,
+    api_login_required,
+    api_login_optional,
+    APIERROR,
+)
+from pagure.api.utils import _get_repo, _check_token, _check_plugin
+
+_log = logging.getLogger(__name__)
+
+# List of ignored form fields, these fields will be not returned in response
+IGNORED_FIELDS = ["active"]
+
+
+def _filter_fields(plugin):
+    """
+    Filter IGNORED_FIELDS from form and return list of the valid fields.
+
+    :arg plugin: plugin class from which to read fields
+    :type plugin: plugin class
+    :return: list of valid fields
+    """
+    fields = []
+    for field in plugin.form_fields:
+        if field not in IGNORED_FIELDS:
+            fields.append(field)
+
+    return fields
+
+
+@API.route("/<repo>/settings/<plugin>/install", methods=["POST"])
+@API.route("/<namespace>/<repo>/settings/<plugin>/install", methods=["POST"])
+@API.route(
+    "/fork/<username>/<repo>/settings/<plugin>/install", methods=["POST"]
+)
+@API.route(
+    "/fork/<username>/<namespace>/<repo>/settings/<plugin>/install",
+    methods=["POST"],
+)
+@api_login_required(acls=["modify_project"])
+@api_method
+def api_install_plugin(repo, plugin, username=None, namespace=None):
+    """
+    Install plugin
+    --------------
+    Install a plugin to a repository.
+
+    ::
+
+        POST /api/0/<repo>/settings/<plugin>/install
+        POST /api/0/<namespace>/<repo>/settings/<plugin>/install
+
+    ::
+
+        POST /api/0/fork/<username>/<repo>/settings/<plugin>/install
+        POST /api/0/fork/<username>/<namespace>/<repo>/settings/<plugin>
+             /install
+
+    Sample response
+    ^^^^^^^^^^^^^^^
+
+    ::
+
+        {
+          "plugin": {
+            "mail_to": "serg@wh40k.com"
+          },
+          "message": "Hook 'Mail' activated"
+        }
+
+    """
+    output = {}
+    repo = _get_repo(repo, username, namespace)
+    _check_token(repo, project_token=False)
+    plugin = _check_plugin(repo, plugin)
+
+    fields = []
+    new = True
+    dbobj = plugin.db_object()
+
+    if hasattr(repo, plugin.backref):
+        dbobj = getattr(repo, plugin.backref)
+
+        # There should always be only one, but let's double check
+        if dbobj:
+            new = False
+        else:
+            dbobj = plugin.db_object()
+
+    form = plugin.form(obj=dbobj, csrf_enabled=False)
+    form.active.data = True
+    for field in plugin.form_fields:
+        fields.append(getattr(form, field))
+
+    if form.validate_on_submit():
+        form.populate_obj(obj=dbobj)
+
+        if new:
+            dbobj.project_id = repo.id
+            flask.g.session.add(dbobj)
+        try:
+            flask.g.session.flush()
+        except SQLAlchemyError:  # pragma: no cover
+            flask.g.session.rollback()
+            _log.exception("Could not add plugin %s", plugin.name)
+            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
+
+        try:
+            # Set up the main script if necessary
+            plugin.set_up(repo)
+            # Install the plugin itself
+            plugin.install(repo, dbobj)
+        except pagure.exceptions.FileNotFoundException as err:
+            flask.g.session.rollback()
+            _log.exception(err)
+            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
+
+        try:
+            flask.g.session.commit()
+            output["message"] = "Hook '%s' activated" % plugin.name
+            output["plugin"] = {
+                field: form[field].data for field in _filter_fields(plugin)
+            }
+        except SQLAlchemyError as err:  # pragma: no cover
+            flask.g.session.rollback()
+            _log.exception(err)
+            raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
+
+    else:
+        raise pagure.exceptions.APIError(
+            400, error_code=APIERROR.EINVALIDREQ, errors=form.errors
+        )
+
+    jsonout = flask.jsonify(output)
+    return jsonout
+
+
+@API.route("/<repo>/settings/<plugin>/remove", methods=["POST"])
+@API.route("/<namespace>/<repo>/settings/<plugin>/remove", methods=["POST"])
+@API.route(
+    "/fork/<username>/<repo>/settings/<plugin>/remove", methods=["POST"]
+)
+@API.route(
+    "/fork/<username>/<namespace>/<repo>/settings/<plugin>/remove",
+    methods=["POST"],
+)
+@api_login_required(acls=["modify_project"])
+@api_method
+def api_remove_plugin(repo, plugin, username=None, namespace=None):
+    """
+    Remove plugin
+    --------------
+    Remove a plugin from repository.
+
+    ::
+
+        POST /api/0/<repo>/settings/<plugin>/remove
+        POST /api/0/<namespace>/<repo>/settings/<plugin>/remove
+
+    ::
+
+        POST /api/0/fork/<username>/<repo>/settings/<plugin>/remove
+        POST /api/0/fork/<username>/<namespace>/<repo>/settings/<plugin>
+             /remove
+
+    Sample response
+    ^^^^^^^^^^^^^^^
+
+    ::
+
+        {
+          "plugin": {
+            "mail_to": "serg@wh40k.com"
+          },
+          "message": "Hook 'Mail' deactivated"
+        }
+
+    """
+    output = {}
+    repo = _get_repo(repo, username, namespace)
+    _check_token(repo, project_token=False)
+    plugin = _check_plugin(repo, plugin)
+
+    dbobj = plugin.db_object()
+
+    enabled_plugins = {
+        plugin[0]: plugin[1]
+        for plugin in plugins_lib.get_enabled_plugins(repo)
+    }
+
+    # If the plugin is not installed raise error
+    if plugin not in enabled_plugins.keys():
+        raise pagure.exceptions.APIError(
+            400, error_code=APIERROR.EPLUGINNOTINSTALLED
+        )
+
+    if enabled_plugins[plugin]:
+        dbobj = enabled_plugins[plugin]
+
+    form = plugin.form(obj=dbobj)
+    form.active.data = False
+
+    try:
+        plugin.remove(repo)
+    except pagure.exceptions.FileNotFoundException as err:
+        flask.g.session.rollback()
+        _log.exception(err)
+        raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR)
+
+    try:
+        flask.g.session.commit()
+        output["message"] = "Hook '%s' deactivated" % plugin.name
+        output["plugin"] = {
+            field: form[field].data for field in _filter_fields(plugin)
+        }
+    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("/<namespace>/<repo>/settings/plugins")
+@API.route("/fork/<username>/<repo>/settings/plugins")
+@API.route("/<repo>/settings/plugins")
+@API.route("/fork/<username>/<namespace>/<repo>/settings/plugins")
+@api_login_optional()
+@api_method
+def api_view_plugins_project(repo, username=None, namespace=None):
+    """
+    List project's plugins
+    ----------------------
+    List installed plugins on a project.
+
+    ::
+
+        GET /api/0/<repo>/settings/plugins
+        GET /api/0/<namespace>/<repo>/settings/plugins
+
+    ::
+
+        GET /api/0/fork/<username>/<repo>/settings/plugins
+        GET /api/0/fork/<username>/<namespace>/<repo>/settings/plugins
+
+    Sample response
+    ^^^^^^^^^^^^^^^
+
+    ::
+
+        {
+            'plugins':
+            [
+                {
+                    'Mail':
+                    {
+                        'mail_to': 'serg@wh40k.com'
+                    }
+                }
+            ],
+            'total_plugins': 1
+        }
+
+    """
+    repo = _get_repo(repo, username, namespace)
+
+    plugins = {
+        plugin[0]: plugin[1]
+        for plugin in plugins_lib.get_enabled_plugins(repo)
+    }
+    output = {}
+
+    output["plugins"] = []
+
+    for (plugin, dbobj) in plugins.items():
+        if dbobj:
+            form = plugin.form(obj=dbobj)
+            fields = _filter_fields(plugin)
+            output["plugins"].append(
+                {plugin.name: {field: form[field].data for field in fields}}
+            )
+
+    output["total_plugins"] = len(output["plugins"])
+
+    jsonout = flask.jsonify(output)
+    return jsonout
+
+
+@API.route("/_plugins")
+@api_method
+def api_view_plugins():
+    """
+    List plugins
+    ------------
+    List every plugin available in this pagure instance. For each plugin their
+    name is provided as well as the name of the argument
+    to provide to enable/disable them.
+
+    ::
+
+        GET /api/0/plugins
+
+    Sample response
+    ^^^^^^^^^^^^^^^
+
+    ::
+
+        {
+            'plugins': [
+                {
+                    'Block Un-Signed commits': [
+                    ]
+                },
+                {
+                    'Block non fast-forward pushes': [
+                        'branches',
+                    ]
+                },
+                {
+                    'Fedmsg': [
+                    ]
+                },
+            ],
+            'total_issues': 3
+    }
+
+    """
+    plugins = plugins_lib.get_plugin_names()
+
+    output = {}
+
+    output["total_plugins"] = len(plugins)
+    output["plugins"] = []
+
+    for plugin_name in plugins:
+        # Skip plugins that are disabled
+        if plugin_name in pagure.config.config.get("DISABLED_PLUGINS", []):
+            continue
+        plugin = plugins_lib.get_plugin(plugin_name)
+        fields = _filter_fields(plugin)
+        output["plugins"].append({plugin_name: fields})
+
+    jsonout = flask.jsonify(output)
+    return jsonout

+ 32 - 1
pagure/api/utils.py

@@ -15,7 +15,8 @@ import logging
 
 
 import pagure.exceptions
-import pagure.lib.query
+
+from pagure.lib import plugins
 
 from pagure.config import config as pagure_config
 from pagure.api import APIERROR, get_authorized_api_project
@@ -233,3 +234,33 @@ def _check_private_pull_request_access(request):
         raise pagure.exceptions.APIError(
             403, error_code=APIERROR.EPRNOTALLOWED
         )
+
+
+def _check_plugin(repo, plugin):
+    """
+    Check if plugin exists.
+
+    :param repo: Repository object
+    :param plugin: Plugin class
+    :return plugin object
+    """
+    plugin = plugins.get_plugin(plugin)
+    if not plugin:
+        raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPLUGIN)
+
+    if repo.private and plugin.name == "Pagure CI":
+        raise pagure.exceptions.APIError(
+            404, error_code=APIERROR.EPLUGINDISABLED
+        )
+
+    if plugin.name in pagure.config.config.get("DISABLED_PLUGINS", []):
+        raise pagure.exceptions.APIError(
+            404, error_code=APIERROR.EPLUGINDISABLED
+        )
+
+    if plugin.name == "default":
+        raise pagure.exceptions.APIError(
+            403, error_code=APIERROR.EPLUGINCHANGENOTALLOWED
+        )
+
+    return plugin

+ 12 - 0
pagure/templates/api.html

@@ -88,6 +88,18 @@
         {% endfor %}
         </div>
 
+        <h2>
+            Plugins
+            <a name="plugins" title="Permalink to this headline" href="#plugins">
+                <span class="fa fa-link"></span>
+            </a>
+        </h2>
+        <div class="accordion">
+            {% for html in plugins %}
+            {{ html | InsertDiv | safe }}
+            {% endfor %}
+        </div>
+
         {% if config.get('PAGURE_CI_SERVICES') %}
         <h2>
             Continous Integration Services

+ 5 - 1
tests/test_pagure_flask_api.py

@@ -241,7 +241,7 @@ class PagureFlaskApitests(tests.SimplePagureTest):
         output = self.app.get("/api/0/-/error_codes")
         self.assertEqual(output.status_code, 200)
         data = json.loads(output.get_data(as_text=True))
-        self.assertEqual(len(data), 37)
+        self.assertEqual(len(data), 41)
         self.assertEqual(
             sorted(data.keys()),
             sorted(
@@ -283,6 +283,10 @@ class PagureFlaskApitests(tests.SimplePagureTest):
                     "ETRACKERREADONLY",
                     "EUBLOCKED",
                     "EREBASENOTALLOWED",
+                    "ENOPLUGIN",
+                    "EPLUGINDISABLED",
+                    "EPLUGINCHANGENOTALLOWED",
+                    "EPLUGINNOTINSTALLED",
                 ]
             ),
         )

+ 190 - 0
tests/test_pagure_flask_api_plugins_install.py

@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2019 - Copyright Red Hat Inc
+
+ Authors:
+   Michal Konecny <mkonecny@redhat.com>
+
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+import datetime
+import unittest
+import sys
+import os
+import json
+
+from mock import patch, MagicMock
+
+sys.path.insert(
+    0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
+)
+
+import pagure.lib.query  # noqa: E402
+import tests  # noqa: E402
+
+
+class PagureFlaskApiPluginInstalltests(tests.Modeltests):
+    """ Tests for the flask API of pagure for installing a plugin
+    """
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def setUp(self):
+        """ Set up the environnment, ran before every tests. """
+        super(PagureFlaskApiPluginInstalltests, self).setUp()
+
+        tests.create_projects(self.session)
+        tests.create_tokens(self.session)
+        tests.create_tokens_acl(self.session)
+
+        # Create project-less token for user foo
+        item = pagure.lib.model.Token(
+            id="project-less-foo",
+            user_id=2,
+            project_id=None,
+            expiration=datetime.datetime.utcnow()
+            + datetime.timedelta(days=30),
+        )
+        self.session.add(item)
+        self.session.commit()
+        tests.create_tokens_acl(self.session, token_id="project-less-foo")
+
+        # Create project-specific token for user foo
+        item = pagure.lib.model.Token(
+            id="project-specific-foo",
+            user_id=2,
+            project_id=1,
+            expiration=datetime.datetime.utcnow()
+            + datetime.timedelta(days=30),
+        )
+        self.session.add(item)
+        self.session.commit()
+        tests.create_tokens_acl(self.session, token_id="project-specific-foo")
+
+    def test_install_plugin_own_project_no_data(self):
+        """ Test installing a new plugin on a project for which you're the
+        main maintainer.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token aaabbbcccddd"}
+
+        # Install a plugin on /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test/settings/Mail/install", headers=headers
+        )
+        self.assertEqual(output.status_code, 400)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            pagure.api.APIERROR.EINVALIDREQ.name, data["error_code"]
+        )
+        self.assertEqual(pagure.api.APIERROR.EINVALIDREQ.value, data["error"])
+        self.assertEqual(
+            data["errors"], {"mail_to": ["This field is required."]}
+        )
+
+    def test_install_plugin_own_project(self):
+        """ Test installing a new plugin on a project for which you're the
+        main maintainer.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token aaabbbcccddd"}
+
+        # complete data set
+        data = {"mail_to": "serg@wh40k.com"}
+
+        # Create an issue on /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test/settings/Mail/install", headers=headers, data=data
+        )
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugin": {"mail_to": "serg@wh40k.com"},
+                "message": "Hook 'Mail' activated",
+            },
+        )
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def test_install_plugin_someone_else_project_project_less_token(self):
+        """ Test installing a new plugin on a project with which you have
+        nothing to do.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token project-less-foo"}
+
+        # Install a plugin on /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test/settings/Prevent creating new branches by git push/"
+            "install",
+            headers=headers,
+        )
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugin": {},
+                "message": "Hook 'Prevent creating new branches by git push' "
+                "activated",
+            },
+        )
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def test_install_plugin_project_specific_token(self):
+        """ Test installing a new plugin on a project with a regular
+        project-specific token.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token project-specific-foo"}
+
+        # complete data set
+        data = {"mail_to": "serg@wh40k.com"}
+
+        # Create an issue on /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test/settings/Mail/install", headers=headers, data=data
+        )
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugin": {"mail_to": "serg@wh40k.com"},
+                "message": "Hook 'Mail' activated",
+            },
+        )
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def test_install_plugin_invalid_project_specific_token(self):
+        """ Test installing a new plugin on a project with a regular
+        project-specific token but for another project.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token project-specific-foo"}
+
+        # complete data set
+        data = {"mail_to": "serg@wh40k.com"}
+
+        # Create an issue on /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test2/settings/Mail/install", headers=headers, data=data
+        )
+        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"])
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 190 - 0
tests/test_pagure_flask_api_plugins_remove.py

@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2019 - Copyright Red Hat Inc
+
+ Authors:
+   Michal Konecny <mkonecny@redhat.com>
+
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+import datetime
+import unittest
+import sys
+import os
+import json
+
+from mock import patch, MagicMock
+
+sys.path.insert(
+    0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
+)
+
+import pagure.lib.plugins as plugins  # noqa: E402
+import pagure.lib.query  # noqa: E402
+import tests  # noqa: E402
+
+
+class PagureFlaskApiPluginRemovetests(tests.Modeltests):
+    """ Tests for the flask API of pagure for removing a plugin
+    """
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def setUp(self):
+        """ Set up the environnment, ran before every tests. """
+        super(PagureFlaskApiPluginRemovetests, self).setUp()
+
+        tests.create_projects(self.session)
+        tests.create_tokens(self.session)
+        tests.create_tokens_acl(self.session)
+
+        # Create project-less token for user foo
+        item = pagure.lib.model.Token(
+            id="project-less-foo",
+            user_id=2,
+            project_id=None,
+            expiration=datetime.datetime.utcnow()
+            + datetime.timedelta(days=30),
+        )
+        self.session.add(item)
+        self.session.commit()
+        tests.create_tokens_acl(self.session, token_id="project-less-foo")
+
+        # Create project-specific token for user foo
+        item = pagure.lib.model.Token(
+            id="project-specific-foo",
+            user_id=2,
+            project_id=1,
+            expiration=datetime.datetime.utcnow()
+            + datetime.timedelta(days=30),
+        )
+        self.session.add(item)
+        self.session.commit()
+
+        # Install plugin
+        repo = pagure.lib.query.get_authorized_project(self.session, "test")
+        plugin = plugins.get_plugin("Mail")
+        plugin.set_up(repo)
+        dbobj = plugin.db_object()
+        dbobj.active = True
+        dbobj.project_id = repo.id
+        dbobj.mail_to = "serg@wh40k.com"
+        plugin.install(repo, dbobj)
+        self.session.add(dbobj)
+        self.session.commit()
+        tests.create_tokens_acl(self.session, token_id="project-specific-foo")
+
+    def test_remove_plugin_own_project_plugin_not_installed(self):
+        """ Test removing a plugin from a project for which you're the
+        main maintainer and the plugin is not installed.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token aaabbbcccddd"}
+
+        # Remove a plugin from /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test/settings/IRC/remove", headers=headers
+        )
+        self.assertEqual(output.status_code, 400)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            pagure.api.APIERROR.EPLUGINNOTINSTALLED.name, data["error_code"]
+        )
+        self.assertEqual(
+            pagure.api.APIERROR.EPLUGINNOTINSTALLED.value, data["error"]
+        )
+
+    def test_remove_plugin_own_project(self):
+        """ Test removing a plugin from a project for which you're the
+        main maintainer.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token aaabbbcccddd"}
+
+        # Remove a plugin from /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test/settings/Mail/remove", headers=headers
+        )
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugin": {"mail_to": "serg@wh40k.com"},
+                "message": "Hook 'Mail' deactivated",
+            },
+        )
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def test_remove_plugin_someone_else_project_project_less_token(self):
+        """ Test removing a plugin from a project with which you have
+        nothing to do.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token project-less-foo"}
+
+        # Remove a plugin from /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test/settings/Mail/" "remove", headers=headers
+        )
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugin": {"mail_to": "serg@wh40k.com"},
+                "message": "Hook 'Mail' deactivated",
+            },
+        )
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def test_remove_plugin_project_specific_token(self):
+        """ Test removing a plugin from a project with a regular
+        project-specific token.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token project-specific-foo"}
+
+        # Remove a plugin from /test/ where pingou is the main admin
+        output = self.app.post(
+            "/api/0/test/settings/Mail/remove", headers=headers
+        )
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugin": {"mail_to": "serg@wh40k.com"},
+                "message": "Hook 'Mail' deactivated",
+            },
+        )
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def test_remove_plugin_invalid_project_specific_token(self):
+        """ Test removing a plugin from a project with a regular
+        project-specific token but for another project.
+        """
+
+        # pingou's token with all the ACLs
+        headers = {"Authorization": "token project-specific-foo"}
+
+        # Remove a plugin from /test2/
+        output = self.app.post(
+            "/api/0/test2/settings/Mail/remove", 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"])
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 115 - 0
tests/test_pagure_flask_api_plugins_view.py

@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2019 - Copyright Red Hat Inc
+
+ Authors:
+   Michal Konecny <mkonecny@redhat.com>
+
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+import sys
+import os
+import json
+
+from mock import patch
+
+sys.path.insert(
+    0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
+)
+
+import tests  # noqa: E402
+
+
+class PagureFlaskApiPluginViewtests(tests.Modeltests):
+    """ Tests for the flask API of pagure for viewing plugins
+    """
+
+    def test_view_plugin(self):
+        """ Test viewing every plugin available in pagure.
+        """
+
+        output = self.app.get("/api/0/_plugins")
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugins": [
+                    {"Block Un-Signed commits": []},
+                    {"Block non fast-forward pushes": ["branches"]},
+                    {"Fedmsg": []},
+                    {
+                        "IRC": [
+                            "server",
+                            "port",
+                            "room",
+                            "nick",
+                            "nick_pass",
+                            "join",
+                            "ssl",
+                        ]
+                    },
+                    {"Mail": ["mail_to"]},
+                    {"Mirroring": ["target", "public_key", "last_log"]},
+                    {"Pagure": []},
+                    {
+                        "Pagure CI": [
+                            "ci_type",
+                            "ci_url",
+                            "ci_job",
+                            "active_commit",
+                            "active_pr",
+                        ]
+                    },
+                    {"Pagure requests": []},
+                    {"Pagure tickets": []},
+                    {"Prevent creating new branches by git push": []},
+                    {"Read the Doc": ["api_url", "api_token", "branches"]},
+                ],
+                "total_plugins": 12,
+            },
+        )
+
+    @patch.dict("pagure.config.config", {"DISABLED_PLUGINS": ["IRC"]})
+    def test_view_plugin_disabled(self):
+        """ Test viewing every plugin available in pagure with one plugin disabled.
+        """
+
+        output = self.app.get("/api/0/_plugins")
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugins": [
+                    {"Block Un-Signed commits": []},
+                    {"Block non fast-forward pushes": ["branches"]},
+                    {"Fedmsg": []},
+                    {"Mail": ["mail_to"]},
+                    {"Mirroring": ["target", "public_key", "last_log"]},
+                    {"Pagure": []},
+                    {
+                        "Pagure CI": [
+                            "ci_type",
+                            "ci_url",
+                            "ci_job",
+                            "active_commit",
+                            "active_pr",
+                        ]
+                    },
+                    {"Pagure requests": []},
+                    {"Pagure tickets": []},
+                    {"Prevent creating new branches by git push": []},
+                    {"Read the Doc": ["api_url", "api_token", "branches"]},
+                ],
+                "total_plugins": 12,
+            },
+        )
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)

+ 81 - 0
tests/test_pagure_flask_api_plugins_view_project.py

@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+
+"""
+ (c) 2019 - Copyright Red Hat Inc
+
+ Authors:
+   Michal Konecny <mkonecny@redhat.com>
+
+"""
+
+from __future__ import unicode_literals, absolute_import
+
+import unittest
+import sys
+import os
+import json
+
+from mock import patch, MagicMock
+
+sys.path.insert(
+    0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
+)
+
+import pagure.lib.plugins as plugins  # noqa: E402
+import pagure.lib.query  # noqa: E402
+import tests  # noqa: E402
+
+
+class PagureFlaskApiPluginViewProjecttests(tests.Modeltests):
+    """ Tests for the flask API of pagure for viewing enabled plugins on project
+    """
+
+    @patch("pagure.lib.notify.send_email", MagicMock(return_value=True))
+    def setUp(self):
+        """ Set up the environnment, ran before every tests. """
+        super(PagureFlaskApiPluginViewProjecttests, self).setUp()
+
+        tests.create_projects(self.session)
+
+    def test_view_plugin_on_project(self):
+        """ Test viewing plugins on a project.
+        """
+
+        # Install plugin
+        repo = pagure.lib.query.get_authorized_project(self.session, "test")
+        plugin = plugins.get_plugin("Mail")
+        plugin.set_up(repo)
+        dbobj = plugin.db_object()
+        dbobj.active = True
+        dbobj.project_id = repo.id
+        dbobj.mail_to = "serg@wh40k.com"
+        plugin.install(repo, dbobj)
+        self.session.add(dbobj)
+        self.session.commit()
+
+        # Retrieve all plugins on project
+        output = self.app.get("/api/0/test/settings/plugins")
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(
+            data,
+            {
+                "plugins": [{"Mail": {"mail_to": "serg@wh40k.com"}}],
+                "total_plugins": 1,
+            },
+        )
+
+    def test_viewing_plugin_on_project_no_plugin(self):
+        """ Test viewing plugins on a project, which doesn't
+        have any installed.
+        """
+
+        # Retrieve all plugins on project
+        output = self.app.get("/api/0/test/settings/plugins")
+        self.assertEqual(output.status_code, 200)
+        data = json.loads(output.get_data(as_text=True))
+        self.assertEqual(data, {"plugins": [], "total_plugins": 0})
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)