Repository: opsdroid/opsdroid (language: python)

Issue:
Add a gitlab connector to opsdroid
At work, we use opsdroid for some things like nuking cloud accounts so devs can test things with a fresh account every time. We have moved to gitlab a couple months ago and would like to expand our use of opsdroid to automate some of our workflows.

With this in mind, we should add a gitlab connector to opsdroid so we can get events from gitlab and create skills based on those events.


Discussion / hints:

Fix patch:
diff --git a/docs/_static/custom.css b/docs/_static/custom.css
index de84b81b5..f4f3f0ce5 100644
--- a/docs/_static/custom.css
+++ b/docs/_static/custom.css
@@ -5,3 +5,7 @@
     font-size: 12px;
     line-height: 1.8;
 }
+
+dl {
+    margin-bottom: 15px !important;
+}
\ No newline at end of file
diff --git a/docs/connectors/gitlab.md b/docs/connectors/gitlab.md
new file mode 100644
index 000000000..ef63d57ec
--- /dev/null
+++ b/docs/connectors/gitlab.md
@@ -0,0 +1,201 @@
+# GitLab
+
+A connector for [GitLab](https://gitlab.com). 
+
+Note that this connector emits only upports sending messages back
+to the event (Issue, Merge Request), if you need to create more elaborate workflow, please
+use the [GitLab API](https://docs.gitlab.com/ee/api/) in your skill.
+
+## Requirements
+
+- A GitLab Account
+- A repository setup with a [webhook](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html)
+- A [personal Access token](https://gitlab.com/-/profile/personal_access_tokens) if you want to send messages to GitLab
+
+## Configuration
+
+```yaml
+connectors:
+  gitlab:
+    # Optional but recommended
+    webhook-token: "my-very-secret-webhook-secret"
+    # Required if you want opsdroid to reply to issues/MRs
+    token: "<personal access token>"
+```
+
+## Setup Webhook
+
+You need to [expose Opsdroid to the internet](../exposing.md), when you have your url, you can go to your repository, click **Settings** and then select **Webhook**, you can then add the url with the `connector/<connector name>` enpoint. For example, assume that you are using `example.com` as your base url and using this connector with the default name, you can use the following url `https://example.com/connector/gitlab` to receive events from your repository.
+
+You can then choose a secret token - this will be your `webhook-token` in the Opsdroid configuration. It's highly recommended that you choose
+a strong token to validate the requests coming into the `https://example.com/connector/gitlab` endpoint.
+
+Finally, you can choose which events you want Gitlab to send you and choose if you want to turn off SSL verification. It's recommended that
+you use SSL at all times.
+
+## Examples
+
+The GitLab connector allows you to send back a message when an event is triggered. This is a great way to automate some workflows. For example, let's assume that you want to thank every user that opens a new issue in your repository.
+
+Here's a small example of how the opsdroid configuration looks like:
+
+```yaml
+connectors:
+  gitlab:
+  webhook-token: "my-secret-token"
+  token: "my-personal-token"
+skills:
+  thankyou:
+    path: "<skill pwd>"
+```
+
+Then you can create a new folder for your skill and write the following:
+
+```python
+from opsdroid.skill import Skill
+from opsdorid.matchers import match_event
+from opsdroid.connector.gitlab.events import GitlabIssueCreated
+from opsdroid.events import Message
+
+class ThankUser(Skill):
+    def __init__(self, opsdroid, config, *args, **kwargs):
+        super().__init__(opsdroid, config, *args, **kwargs)
+        self.opsdroid = opsdroid
+    
+    @match_event(GitlabIssueCreated)
+    async def say_thank_you(self, event):
+        """Send message to issue, thanking user."""
+        await event.respond(
+            Message(
+                text="Thank you for opening this issue, a team member will be with you shortly!",
+                target=event.target
+            )
+        )
+```
+
+Now let's assume that you want to notify a slack channel every time a label was changed in an MR. This can be useful if you want to alert your team that an MR is ready for code review.
+
+A more robust opsdroid configuration:
+
+```yaml
+connectors:
+  gitlab:
+    webhook-token: "/secret-t0k3n|"
+    token: "<your secret token>"
+  slack:
+    bot-token: "<your token>"
+    socket-mode: false
+    bot-name: "Review-Bot"
+    default-room: "#code-reviews"
+skills:
+  alert-team:
+    path: "<skill pwd>"
+```
+
+The skill will look somewhat similar to the previous example with a few changes:
+
+```python
+from opsdroid.skill import Skill
+from opsdorid.matchers import match_event
+from opsdroid.connector.gitlab.events import GitlabIssueCreated
+from opsdroid.events import Message
+
+class AlertTeam(Skill):
+    def __init__(self, opsdroid, config, *args, **kwargs):
+        super().__init__(opsdroid, config, *args, **kwargs)
+        self.opsdroid = opsdroid
+    
+    @match_event(MRLabeled)
+    async def mr_labeled(self, event):
+        """Send message slack if MR was labeled as code-review."""
+        slack = self.opsdroid.get_connector("slack")
+        if "code-review" in event.labels:
+            await slack.send(
+                Message(
+                    text=f"Hey folks, the MR ({event.url}) was just marked as ready for review!",
+                )
+            )
+```
+
+This skill will trigger every time an MR gets labeled with something but only sends the message to the 
+Slack channel `#code-reviews` when the MR is labeled with the  `code-review` label.
+
+## Events Available
+
+Currently, the GitLab connector handles Merge Requests and Issues events, any other event such as pipeline events, for example, will be retured as `GenericGitlabEvent`.
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.GenericGitlabEvent
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.GitlabIssueCreated
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.GitlabIssueClosed
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.GitlabIssueEdited
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.GitlabIssueLabeled
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.GenericIssueEvent
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.MRCreated
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.MRMerged
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.MRClosed
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.MRLabeled
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.MRApproved
+    :members:
+```
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.events.GenericMREvent
+    :members:
+```
+
+## Reference
+
+This dataclass is used internally within the Gitlab connector, but is worth noting a few things here.
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.GitlabPayload
+    :members:
+```
+
+This is the Connector reference
+
+```eval_rst
+.. autoclass:: opsdroid.connector.gitlab.ConnectorGitlab
+    :members:
+```
\ No newline at end of file
diff --git a/docs/connectors/index.md b/docs/connectors/index.md
index de5f9af0a..88d371fdb 100644
--- a/docs/connectors/index.md
+++ b/docs/connectors/index.md
@@ -7,6 +7,7 @@ Connectors are modules which connect opsdroid to an external event source. This
    :maxdepth: 1
 
    facebook
+   gitlab
    github
    gitter
    mattermost
diff --git a/opsdroid/configuration/example_configuration.yaml b/opsdroid/configuration/example_configuration.yaml
index 0bc8588bf..31d7edbc0 100644
--- a/opsdroid/configuration/example_configuration.yaml
+++ b/opsdroid/configuration/example_configuration.yaml
@@ -176,6 +176,12 @@ connectors:
 #    ssl-verify: false # default: true
 #    connect-timeout: 30 # default: 30
 #
+#  ##GitLab (core)
+#  gitlab:
+#    # Optional but recomended
+#    webhook-token: <your token>
+#    # Required if sending message to gitlab
+#    token: <personal access token>
 #  ## Github (core)
 #  github:
 #    # required
diff --git a/opsdroid/connector/gitlab/__init__.py b/opsdroid/connector/gitlab/__init__.py
new file mode 100644
index 000000000..23f105d45
--- /dev/null
+++ b/opsdroid/connector/gitlab/__init__.py
@@ -0,0 +1,2 @@
+"""Import Gitlab connector."""
+from .connector import ConnectorGitlab, GitlabPayload  # noqa: F401
diff --git a/opsdroid/connector/gitlab/connector.py b/opsdroid/connector/gitlab/connector.py
new file mode 100644
index 000000000..673eab0a8
--- /dev/null
+++ b/opsdroid/connector/gitlab/connector.py
@@ -0,0 +1,391 @@
+"""A connector for Gitlab."""
+import dataclasses
+import json
+
+import logging
+from typing import Optional
+import aiohttp
+
+from aiohttp.web_request import Request
+from aiohttp.web_response import Response
+from opsdroid.connector import Connector, register_event
+from opsdroid.connector.gitlab.events import (
+    GenericGitlabEvent,
+    GenericIssueEvent,
+    GenericMREvent,
+    GitlabIssueClosed,
+    GitlabIssueCreated,
+    GitlabIssueEdited,
+    GitlabIssueLabeled,
+    MRApproved,
+    MRClosed,
+    MRCreated,
+    MRLabeled,
+    MRMerged,
+)
+from opsdroid.const import GITLAB_API_ENDPOINT
+from opsdroid.core import OpsDroid
+from opsdroid.events import Event
+
+from opsdroid.events import Message
+
+
+@dataclasses.dataclass
+class GitlabPayload:
+    """Generate Gitlab Payload object.
+
+    This dataclass will be used to handle the gitlab payload and to
+    build the object from the payload returned by Gitlab webhooks.
+    The main reason for that is because Gitlab might return different
+    structures depending on the event that was sent.
+
+    Note:
+      * ``url`` is refered to the event url (for example the MR url)
+      * ``target`` is the API endpoint to send a reply by using
+        ``ConnectorGitlab.send_message.``
+
+    """
+
+    attributes: dict
+    changes: dict
+    labels: list
+    project_name: str
+    url: str
+    target: str
+    description: Optional[str]
+    title: Optional[str]
+    username: str
+    action: Optional[str]
+    raw_payload: dict
+
+    @classmethod
+    def from_dict(cls, payload: dict):
+        """Create the GitlabPayload object from a dictionary."""
+        labels = payload.get("labels", [])
+        attributes = payload.get("object_attributes", {})
+        changes = payload.get("changes", {})
+        project_name = payload.get("project", {}).get("name", "")
+        username = payload.get("user", {}).get("username", "") or payload.get(
+            "user_username", ""
+        )
+        description = attributes.get("description")
+        title = attributes.get("title")
+        action = attributes.get("action") or attributes.get("state")
+        event_type = payload.get("event_type")
+        project_id = payload.get("project", {}).get("id")
+        url = attributes.get("url") or payload.get("project", {}).get("homepage", "")
+        target = f"{GITLAB_API_ENDPOINT}/projects/{project_id}/{event_type}s/{attributes.get('iid')}"
+
+        return cls(
+            attributes=attributes,
+            changes=changes,
+            labels=[label["title"] for label in labels],
+            project_name=project_name,
+            url=url,
+            target=target,
+            description=description,
+            title=title,
+            username=username,
+            action=action,
+            raw_payload=payload,
+        )
+
+
+CONFIG_SCHEMA = {"webhook-token": str, "forward-url": str, "token": str}
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ConnectorGitlab(Connector):
+    """A connector for Gitlab."""
+
+    def __init__(self, config: dict, opsdroid: Optional[OpsDroid] = None):
+        _LOGGER.debug(_("Starting Gitlab connector."))
+        super().__init__(config, opsdroid=opsdroid)
+        self.name = config.get("name", "gitlab")
+        self.opsdroid = opsdroid
+        self.webhook_token = config.get("webhook-token")
+        self.token = config.get("token")
+        try:
+            self.base_url = opsdroid.config["web"]["base-url"]  # type: ignore
+        except (KeyError, AttributeError):
+            self.base_url = config.get("forward-url")
+
+    async def connect(self):
+        """Connect method for Gitlab.
+
+        This connector is using webhook events only. This method is used only to create
+        the routes for the webhooks.
+
+        """
+        if self.opsdroid and self.opsdroid.web_server:
+            self.opsdroid.web_server.web_app.router.add_post(
+                f"/connector/{self.name}", self.gitlab_webhook_handler
+            )
+
+            _LOGGER.debug(
+                _(f"Created webhook route '/connector/{self.name}' successfully.")
+            )
+
+    async def validate_request(self, request) -> bool:
+        """Validate webhook request.
+
+        Gitlab allow us to add a secret token to be added to the X-Gitlab-Token HTTP header,
+        this method will simply check the request and compare the value of this header against
+        the token that the user specified in the configuration.
+
+        If no secret is specified in opsdroid configuration, then we are
+        assuming that every request is a valid one. Note that, you should
+        add a secret otherwise anyone could submit a POST request to the
+        URL and pretent to be GitLab.
+
+        """
+        gitlab_token = request.headers.get("X-Gitlab-Token")
+
+        if self.webhook_token:
+            return gitlab_token == self.webhook_token
+        return True
+
+    async def listen(self):
+        """Listen method of the connector.
+
+        Since we are using webhook events, this method does nothing.
+        """
+
+    async def gitlab_webhook_handler(self, request: Request) -> Response:
+        """Handle event from Gitlab webhooks."""
+        valid = await self.validate_request(request)
+        payload = None
+        try:
+            payload = await request.json()
+            _LOGGER.info(payload)
+        except json.JSONDecodeError:
+            _LOGGER.error(_("Unable to decode json!"))
+            return Response(
+                text=json.dumps("Unable to parse event, received bad data"), status=400
+            )
+        except Exception as error:
+            _LOGGER.exception(
+                _(
+                    f"Unable to get JSON from request. Reason - {str(error)}. Request is: {request}"
+                )
+            )
+            return Response(
+                text=json.dumps("Unable to parse data received."), status=400
+            )
+
+        if valid and payload:
+            gitlab_payload = GitlabPayload.from_dict(payload=payload)
+            if payload.get("event_type") == "merge_request":
+                event = await self.handle_merge_request_event(payload=gitlab_payload)
+            elif payload.get("event_type") == "issue":
+                event = await self.handle_issue_event(payload=gitlab_payload)
+            else:
+                event = GenericGitlabEvent(
+                    project=gitlab_payload.project_name,
+                    user=gitlab_payload.username,
+                    title=gitlab_payload.title,
+                    description=gitlab_payload.description,
+                    labels=gitlab_payload.labels,
+                    url=gitlab_payload.url,
+                    target=gitlab_payload.target,
+                    connector=self,
+                    raw_event=gitlab_payload.raw_payload,
+                )
+            await self.opsdroid.parse(event)
+            return Response(text=json.dumps("Received"), status=200)
+        return Response(text=json.dumps("Unauthorized"), status=401)
+
+    async def handle_merge_request_event(self, payload: GitlabPayload) -> Event:
+        """Handle Merge Request Events.
+
+        When a user opens a MR Gitlab will throw an event, then when something
+        happens within that particular MR (labels, commits, milestones), Gitlab
+        will emit new events. This method handles all of these events based on
+        the payload and builds the appropriate opsdroid events.
+
+        """
+        labels = payload.changes.get("labels", {})
+        if payload.action == "approved":
+            event = MRApproved(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        elif payload.action == "opened":
+            event = MRCreated(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        elif payload.action == "update" and labels:
+            updated_labels = labels.get("current", [])
+            labels = [label["title"] for label in updated_labels]
+            event = MRLabeled(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        elif payload.action == "merge" and payload.attributes.get("state") == "merged":
+            event = MRMerged(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        elif payload.action == "close" and payload.attributes.get("state") == "closed":
+            event = MRClosed(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        else:
+            event = GenericMREvent(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        return event
+
+    async def handle_issue_event(self, payload: GitlabPayload) -> Event:
+        """Handle issues events."""
+        labels = payload.changes.get("labels", {})
+        if payload.action == "opened":
+            event = GitlabIssueCreated(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        elif payload.action == "close":
+            event = GitlabIssueClosed(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        elif payload.action == "update" and payload.changes.get("last_edited_at"):
+            event = GitlabIssueEdited(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        elif payload.action == "update" and labels:
+            updated_labels = labels.get("current", [])
+            labels = [label["title"] for label in updated_labels]
+            event = GitlabIssueLabeled(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        else:
+            event = GenericIssueEvent(
+                project=payload.project_name,
+                user=payload.username,
+                title=payload.title,
+                description=payload.description,
+                labels=payload.labels,
+                url=payload.url,
+                target=payload.target,
+                connector=self,
+                raw_event=payload.raw_payload,
+            )
+        return event
+
+    @register_event(Message)
+    async def send_message(self, message):
+        """Respond with a message.
+
+        Gitlab uses notes to push messages to Issues, Snippets,
+        Merge Requests and Epics. The URL is usually <event target>/notes
+        so we can simply get the target from the Message event and use that
+        to reply.
+
+        Note that if we don't have a target then we won't return any message.
+
+        """
+        if not self.token:
+            _LOGGER.error(
+                _("Unable to reply to GitLab, you must specify an account token!")
+            )
+        else:
+            _LOGGER.debug(_("Responding via Gitlab"))
+            headers = {"PRIVATE-TOKEN": self.token, "Content-Type": "application/json"}
+            async with aiohttp.ClientSession(trust_env=True) as session:
+                resp = await session.post(
+                    f"{message.target}/notes",
+                    params={"body": message.text},
+                    headers=headers,
+                )
+                if resp.status == 201:
+                    _LOGGER.info(
+                        _(
+                            f"Message '{message.text}' sent to GitLab to '{message.target}'."
+                        )
+                    )
+                    return True
+                else:
+                    _LOGGER.error(
+                        _(
+                            f"Unable to send '{message.text}' to GitLab. Received status code: {resp.status}"
+                        )
+                    )
+        return False
diff --git a/opsdroid/connector/gitlab/events.py b/opsdroid/connector/gitlab/events.py
new file mode 100644
index 000000000..5de1f5570
--- /dev/null
+++ b/opsdroid/connector/gitlab/events.py
@@ -0,0 +1,280 @@
+"""events.Events for the Gitlab Connector."""
+from typing import Optional
+
+from opsdroid import events
+
+
+class GenericGitlabEvent(events.Event):
+    """Event class that triggers when an unhandled event is sent."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class GitlabIssueCreated(events.Event):
+    """Event class that triggers when a new issue is created."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class GitlabIssueClosed(events.Event):
+    """Event class that triggers when an issue is closed."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class GitlabIssueEdited(events.Event):
+    """Event class that triggers when an issue is edited."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class GitlabIssueLabeled(events.Event):
+    """Event class that triggers when an issue is labeled."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class GenericIssueEvent(events.Event):
+    """Event class that triggers when any other issue action happen."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class MRCreated(events.Event):
+    """Event class that triggers when a MR is created."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class MRMerged(events.Event):
+    """Event class that triggers when a MR is merged."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class MRClosed(events.Event):
+    """Event class that triggers when a MR is closed."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class MRLabeled(events.Event):
+    """Event class that triggers when a MR label is updated."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class MRApproved(events.Event):
+    """Event class that triggers when a MR is approved."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
+
+
+class GenericMREvent(events.Event):
+    """Event class that triggers when a Generic MR events.Event happens."""
+
+    def __init__(
+        self,
+        project: str,
+        user: str,
+        title: Optional[str],
+        description: Optional[str],
+        labels: list,
+        url: str,
+        *args,
+        **kwargs
+    ):
+        super().__init__(*args, **kwargs)
+        self.project = project
+        self.user = user
+        self.title = title
+        self.description = description
+        self.labels = labels
+        self.url = url
diff --git a/opsdroid/const.py b/opsdroid/const.py
index 956a2ab31..98c7cc5e0 100644
--- a/opsdroid/const.py
+++ b/opsdroid/const.py
@@ -50,3 +50,4 @@
 TWITCH_API_ENDPOINT = "https://api.twitch.tv/helix"
 TWITCH_IRC_MESSAGE_REGEX = r"@.*;id=(?P<message_id>.*);m.*user-id=(?P<user_id>.*);user-type=.*:(?P<user>.*?)!.*PRIVMSG[^:]*:(?P<message>.*)"
 TWITCH_JSON = os.path.join(DEFAULT_ROOT_PATH, "twitch.json")
+GITLAB_API_ENDPOINT = "https://gitlab.com/api/v4"


Test patch:
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/bad_json.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/bad_json.json
new file mode 100644
index 000000000..86d19746d
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/bad_json.json
@@ -0,0 +1,3 @@
+{
+    "test": 1,
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/confidential_issue_created.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/confidential_issue_created.json
new file mode 100644
index 000000000..54000ed38
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/confidential_issue_created.json
@@ -0,0 +1,98 @@
+{
+    "object_kind": "issue",
+    "event_type": "issue",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "author_id": 3612771,
+        "closed_at": null,
+        "confidential": true,
+        "created_at": "2021-11-07T18:10:32.914Z",
+        "description": "",
+        "discussion_locked": null,
+        "due_date": null,
+        "id": 96886238,
+        "iid": 1,
+        "last_edited_at": null,
+        "last_edited_by_id": null,
+        "milestone_id": null,
+        "moved_to_id": null,
+        "duplicated_to_id": null,
+        "project_id": 30456730,
+        "relative_position": 513,
+        "state_id": 1,
+        "time_estimate": 0,
+        "title": "New test issue",
+        "updated_at": "2021-11-07T18:10:47.885Z",
+        "updated_by_id": 3612771,
+        "weight": null,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/issues/1",
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [],
+        "assignee_id": null,
+        "labels": [
+            {
+                "id": 22478741,
+                "title": "test-label",
+                "color": "#009966",
+                "project_id": 30456730,
+                "created_at": "2021-11-07T18:10:43.765Z",
+                "updated_at": "2021-11-07T18:10:43.765Z",
+                "template": false,
+                "description": null,
+                "type": "ProjectLabel",
+                "group_id": null
+            }
+        ],
+        "state": "opened",
+        "severity": "unknown"
+    },
+    "labels": [
+        {
+            "id": 22478741,
+            "title": "test-label",
+            "color": "#009966",
+            "project_id": 30456730,
+            "created_at": "2021-11-07T18:10:43.765Z",
+            "updated_at": "2021-11-07T18:10:43.765Z",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {},
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+    }
+}
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/generic_issue.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/generic_issue.json
new file mode 100644
index 000000000..d7953a8e9
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/generic_issue.json
@@ -0,0 +1,112 @@
+{
+    "object_kind": "issue",
+    "event_type": "issue",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "author_id": 3612771,
+        "closed_at": null,
+        "confidential": false,
+        "created_at": "2021-11-07 18:10:32 UTC",
+        "description": "This should have been filled",
+        "discussion_locked": null,
+        "due_date": null,
+        "id": 96886238,
+        "iid": 1,
+        "last_edited_at": "2021-11-07 20:17:43 UTC",
+        "last_edited_by_id": 3612771,
+        "milestone_id": null,
+        "moved_to_id": null,
+        "duplicated_to_id": null,
+        "project_id": 30456730,
+        "relative_position": 513,
+        "state_id": 1,
+        "time_estimate": 0,
+        "title": "New test issue",
+        "updated_at": "2021-11-17 13:05:43 UTC",
+        "updated_by_id": 3612771,
+        "weight": null,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/issues/1",
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [],
+        "assignee_id": null,
+        "labels": [
+            {
+                "id": 22478741,
+                "title": "test-label",
+                "color": "#009966",
+                "project_id": 30456730,
+                "created_at": "2021-11-07 18:10:43 UTC",
+                "updated_at": "2021-11-07 18:10:43 UTC",
+                "template": false,
+                "description": null,
+                "type": "ProjectLabel",
+                "group_id": null
+            }
+        ],
+        "state": "opened",
+        "severity": "unknown",
+        "action": "reopen"
+    },
+    "labels": [
+        {
+            "id": 22478741,
+            "title": "test-label",
+            "color": "#009966",
+            "project_id": 30456730,
+            "created_at": "2021-11-07 18:10:43 UTC",
+            "updated_at": "2021-11-07 18:10:43 UTC",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {
+        "closed_at": {
+            "previous": "2021-11-07 20:20:06 UTC",
+            "current": null
+        },
+        "state_id": {
+            "previous": 2,
+            "current": 1
+        },
+        "updated_at": {
+            "previous": "2021-11-07 20:20:06 UTC",
+            "current": "2021-11-17 13:05:43 UTC"
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/generic_mr.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/generic_mr.json
new file mode 100644
index 000000000..125b3878b
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/generic_mr.json
@@ -0,0 +1,132 @@
+{
+    "object_kind": "merge_request",
+    "event_type": "merge_request",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https: //secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "assignee_id": null,
+        "author_id": 3612771,
+        "created_at": "2021-11-17 12:57:16 UTC",
+        "description": "This reverts merge request !1",
+        "head_pipeline_id": null,
+        "id": 126521680,
+        "iid": 2,
+        "last_edited_at": null,
+        "last_edited_by_id": null,
+        "merge_commit_sha": null,
+        "merge_error": null,
+        "merge_params": {
+            "force_remove_source_branch": "1"
+        },
+        "merge_status": "can_be_merged",
+        "merge_user_id": null,
+        "merge_when_pipeline_succeeds": false,
+        "milestone_id": null,
+        "source_branch": "revert-5e25ad25",
+        "source_project_id": 30456730,
+        "state_id": 1,
+        "target_branch": "main",
+        "target_project_id": 30456730,
+        "time_estimate": 0,
+        "title": "Revert Merge branch \"test\" into \"main\"",
+        "updated_at": "2021-11-17 13:01:59 UTC",
+        "updated_by_id": null,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/merge_requests/2",
+        "source": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "target": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "last_commit": {
+            "id": "ef91bab5bae1d4fac5c9cb33472188bbdfbe2757",
+            "message": "Revert Merge branch \"test\" into \"main\"\n\nThis reverts merge request !1",
+            "title": "Revert Merge branch \"test\" into \"main\"",
+            "timestamp": "2021-11-17T12:57:09+00:00",
+            "url": "https://gitlab.com/FabioRosado/test-project/-/commit/ef91bab5bae1d4fac5c9cb33472188bbdfbe2757",
+            "author": {
+                "name": "Fabio Rosado",
+                "email": "fabioglrosado@gmail.com"
+            }
+        },
+        "work_in_progress": false,
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [],
+        "state": "opened",
+        "action": "reopen"
+    },
+    "labels": [],
+    "changes": {
+        "state_id": {
+            "previous": 2,
+            "current": 1
+        },
+        "updated_at": {
+            "previous": "2021-11-17 12:58:12 UTC",
+            "current": "2021-11-17 13:01:59 UTC"
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_closed.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_closed.json
new file mode 100644
index 000000000..22d3209a3
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_closed.json
@@ -0,0 +1,106 @@
+{
+        "object_kind": "issue",
+        "event_type": "issue",
+        "user": {
+            "id": 3612771,
+            "name": "Fabio Rosado",
+            "username": "FabioRosado",
+            "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+            "email": "[REDACTED]"
+        },
+        "project": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "object_attributes": {
+            "author_id": 3612771,
+            "closed_at": "2021-11-07 20:20:06 UTC",
+            "confidential": false,
+            "created_at": "2021-11-07 18:10:32 UTC",
+            "description": "This should have been filled",
+            "discussion_locked": null,
+            "due_date": null,
+            "id": 96886238,
+            "iid": 1,
+            "last_edited_at": "2021-11-07 20:17:43 UTC",
+            "last_edited_by_id": 3612771,
+            "milestone_id": null,
+            "moved_to_id": null,
+            "duplicated_to_id": null,
+            "project_id": 30456730,
+            "relative_position": 513,
+            "state_id": 2,
+            "time_estimate": 0,
+            "title": "New test issue",
+            "updated_at": "2021-11-07 20:20:06 UTC",
+            "updated_by_id": 3612771,
+            "weight": null,
+            "url": "https://gitlab.com/FabioRosado/test-project/-/issues/1",
+            "total_time_spent": 0,
+            "time_change": 0,
+            "human_total_time_spent": null,
+            "human_time_change": null,
+            "human_time_estimate": null,
+            "assignee_ids": [],
+            "assignee_id": null,
+            "labels": [
+                {
+                    "id": 22478741,
+                    "title": "test-label",
+                    "color": "#009966",
+                    "project_id": 30456730,
+                    "created_at": "2021-11-07 18:10:43 UTC",
+                    "updated_at": "2021-11-07 18:10:43 UTC",
+                    "template": false,
+                    "description": null,
+                    "type": "ProjectLabel",
+                    "group_id": null
+                }
+            ],
+            "state": "closed",
+            "severity": "unknown",
+            "action": "close"
+        },
+        "labels": [
+            {
+                "id": 22478741,
+                "title": "test-label",
+                "color": "#009966",
+                "project_id": 30456730,
+                "created_at": "2021-11-07 18:10:43 UTC",
+                "updated_at": "2021-11-07 18:10:43 UTC",
+                "template": false,
+                "description": null,
+                "type": "ProjectLabel",
+                "group_id": null
+            }
+        ],
+        "changes": {
+            "closed_at": {"previous": null, "current": "2021-11-07 20:20:06 UTC"},
+            "state_id": {"previous": 1, "current": 2},
+            "updated_at": {
+                "previous": "2021-11-07 20:17:43 UTC",
+                "current": "2021-11-07 20:20:06 UTC"
+            }
+        },
+        "repository": {
+            "name": "test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "description": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project"
+        }
+    }
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_created.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_created.json
new file mode 100644
index 000000000..9478eb8f4
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_created.json
@@ -0,0 +1,98 @@
+{
+    "object_kind": "issue",
+    "event_type": "issue",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "author_id": 3612771,
+        "closed_at": null,
+        "confidential": false,
+        "created_at": "2021-11-07T18:10:32.914Z",
+        "description": "Test description",
+        "discussion_locked": null,
+        "due_date": null,
+        "id": 96886238,
+        "iid": 1,
+        "last_edited_at": null,
+        "last_edited_by_id": null,
+        "milestone_id": null,
+        "moved_to_id": null,
+        "duplicated_to_id": null,
+        "project_id": 30456730,
+        "relative_position": 513,
+        "state_id": 1,
+        "time_estimate": 0,
+        "title": "New test issue",
+        "updated_at": "2021-11-07T18:10:47.885Z",
+        "updated_by_id": 3612771,
+        "weight": null,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/issues/1",
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [],
+        "assignee_id": null,
+        "labels": [
+            {
+                "id": 22478741,
+                "title": "test-label",
+                "color": "#009966",
+                "project_id": 30456730,
+                "created_at": "2021-11-07T18:10:43.765Z",
+                "updated_at": "2021-11-07T18:10:43.765Z",
+                "template": false,
+                "description": null,
+                "type": "ProjectLabel",
+                "group_id": null
+            }
+        ],
+        "state": "opened",
+        "severity": "unknown"
+    },
+    "labels": [
+        {
+            "id": 22478741,
+            "title": "test-label",
+            "color": "#009966",
+            "project_id": 30456730,
+            "created_at": "2021-11-07T18:10:43.765Z",
+            "updated_at": "2021-11-07T18:10:43.765Z",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {},
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_label_update.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_label_update.json
new file mode 100644
index 000000000..94fe9717d
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_label_update.json
@@ -0,0 +1,130 @@
+{
+    "object_kind": "issue",
+    "event_type": "issue",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "author_id": 3612771,
+        "closed_at": null,
+        "confidential": false,
+        "created_at": "2021-11-07 18:10:32 UTC",
+        "description": "This should have been filled",
+        "discussion_locked": null,
+        "due_date": null,
+        "id": 96886238,
+        "iid": 1,
+        "last_edited_at": "2021-11-07 20:17:43 UTC",
+        "last_edited_by_id": 3612771,
+        "milestone_id": null,
+        "moved_to_id": null,
+        "duplicated_to_id": null,
+        "project_id": 30456730,
+        "relative_position": 513,
+        "state_id": 1,
+        "time_estimate": 0,
+        "title": "New test issue",
+        "updated_at": "2021-11-17 13:34:12 UTC",
+        "updated_by_id": 3612771,
+        "weight": null,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/issues/1",
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [],
+        "assignee_id": null,
+        "labels": [
+            {
+                "id": 22478741,
+                "title": "test-label",
+                "color": "#009966",
+                "project_id": 30456730,
+                "created_at": "2021-11-07 18:10:43 UTC",
+                "updated_at": "2021-11-07 18:10:43 UTC",
+                "template": false,
+                "description": null,
+                "type": "ProjectLabel",
+                "group_id": null
+            }
+        ],
+        "state": "opened",
+        "severity": "unknown",
+        "action": "update"
+    },
+    "labels": [
+        {
+            "id": 22478741,
+            "title": "test-label",
+            "color": "#009966",
+            "project_id": 30456730,
+            "created_at": "2021-11-07 18:10:43 UTC",
+            "updated_at": "2021-11-07 18:10:43 UTC",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {
+        "labels": {
+            "previous": [
+                {
+                    "id": 22478750,
+                    "title": "blah",
+                    "color": "#ff0000",
+                    "project_id": 30456730,
+                    "created_at": "2021-11-07 18:17:22 UTC",
+                    "updated_at": "2021-11-07 18:17:22 UTC",
+                    "template": false,
+                    "description": null,
+                    "type": "ProjectLabel",
+                    "group_id": null
+                }
+            ],
+            "current": [
+                {
+                    "id": 22478741,
+                    "title": "test-label",
+                    "color": "#009966",
+                    "project_id": 30456730,
+                    "created_at": "2021-11-07 18:10:43 UTC",
+                    "updated_at": "2021-11-07 18:10:43 UTC",
+                    "template": false,
+                    "description": null,
+                    "type": "ProjectLabel",
+                    "group_id": null
+                }
+            ]
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_labeled.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_labeled.json
new file mode 100644
index 000000000..2715f1531
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_labeled.json
@@ -0,0 +1,125 @@
+{
+    "object_kind": "issue",
+    "event_type": "issue",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "author_id": 3612771,
+        "closed_at": null,
+        "confidential": false,
+        "created_at": "2021-11-17 13:34:41 UTC",
+        "description": "",
+        "discussion_locked": null,
+        "due_date": null,
+        "id": 97484746,
+        "iid": 2,
+        "last_edited_at": null,
+        "last_edited_by_id": null,
+        "milestone_id": null,
+        "moved_to_id": null,
+        "duplicated_to_id": null,
+        "project_id": 30456730,
+        "relative_position": 1026,
+        "state_id": 1,
+        "time_estimate": 0,
+        "title": "test",
+        "updated_at": "2021-11-17 13:35:43 UTC",
+        "updated_by_id": 3612771,
+        "weight": null,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/issues/2",
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [],
+        "assignee_id": null,
+        "labels": [
+            {
+                "id": 22478750,
+                "title": "blah",
+                "color": "#ff0000",
+                "project_id": 30456730,
+                "created_at": "2021-11-07 18:17:22 UTC",
+                "updated_at": "2021-11-07 18:17:22 UTC",
+                "template": false,
+                "description": null,
+                "type": "ProjectLabel",
+                "group_id": null
+            }
+        ],
+        "state": "opened",
+        "severity": "unknown",
+        "action": "update"
+    },
+    "labels": [
+        {
+            "id": 22478750,
+            "title": "blah",
+            "color": "#ff0000",
+            "project_id": 30456730,
+            "created_at": "2021-11-07 18:17:22 UTC",
+            "updated_at": "2021-11-07 18:17:22 UTC",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {
+        "updated_at": {
+            "previous": "2021-11-17 13:35:42 UTC",
+            "current": "2021-11-17 13:35:43 UTC"
+        },
+        "updated_by_id": {
+            "previous": null,
+            "current": 3612771
+        },
+        "labels": {
+            "previous": [],
+            "current": [
+                {
+                    "id": 22478750,
+                    "title": "blah",
+                    "color": "#ff0000",
+                    "project_id": 30456730,
+                    "created_at": "2021-11-07 18:17:22 UTC",
+                    "updated_at": "2021-11-07 18:17:22 UTC",
+                    "template": false,
+                    "description": null,
+                    "type": "ProjectLabel",
+                    "group_id": null
+                }
+            ]
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_message_edited.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_message_edited.json
new file mode 100644
index 000000000..f71b264f8
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/issue_message_edited.json
@@ -0,0 +1,107 @@
+ {
+    "object_kind": "issue",
+    "event_type": "issue",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "author_id": 3612771,
+        "closed_at": null,
+        "confidential": false,
+        "created_at": "2021-11-07 18:10:32 UTC",
+        "description": "This should have been filled",
+        "discussion_locked": null,
+        "due_date": null,
+        "id": 96886238,
+        "iid": 1,
+        "last_edited_at": "2021-11-07 20:17:43 UTC",
+        "last_edited_by_id": 3612771,
+        "milestone_id": null,
+        "moved_to_id": null,
+        "duplicated_to_id": null,
+        "project_id": 30456730,
+        "relative_position": 513,
+        "state_id": 1,
+        "time_estimate": 0,
+        "title": "New test issue",
+        "updated_at": "2021-11-07 20:17:43 UTC",
+        "updated_by_id": 3612771,
+        "weight": null,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/issues/1",
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [],
+        "assignee_id": null,
+        "labels": [
+            {
+                "id": 22478741,
+                "title": "test-label",
+                "color": "#009966",
+                "project_id": 30456730,
+                "created_at": "2021-11-07 18:10:43 UTC",
+                "updated_at": "2021-11-07 18:10:43 UTC",
+                "template": false,
+                "description": null,
+                "type": "ProjectLabel",
+                "group_id": null
+            }
+        ],
+        "state": "opened",
+        "severity": "unknown",
+        "action": "update"
+    },
+    "labels": [
+        {
+            "id": 22478741,
+            "title": "test-label",
+            "color": "#009966",
+            "project_id": 30456730,
+            "created_at": "2021-11-07 18:10:43 UTC",
+            "updated_at": "2021-11-07 18:10:43 UTC",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {
+        "description": {"previous": "", "current": "This should have been filled"},
+        "last_edited_at": {"previous": null, "current": "2021-11-07 20:17:43 UTC"},
+        "last_edited_by_id": {"previous": null, "current": 3612771},
+        "updated_at": {
+            "previous": "2021-11-07 18:10:47 UTC",
+            "current": "2021-11-07 20:17:43 UTC"
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_approved.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_approved.json
new file mode 100644
index 000000000..80c8ed83b
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_approved.json
@@ -0,0 +1,152 @@
+{
+    "object_kind": "merge_request",
+    "event_type": "merge_request",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "assignee_id": 3612771,
+        "author_id": 3612771,
+        "created_at": "2021-11-07 18:14:50 UTC",
+        "description": "",
+        "head_pipeline_id": null,
+        "id": 124957728,
+        "iid": 1,
+        "last_edited_at": null,
+        "last_edited_by_id": null,
+        "merge_commit_sha": null,
+        "merge_error": null,
+        "merge_params": {
+            "force_remove_source_branch": "1"
+        },
+        "merge_status": "can_be_merged",
+        "merge_user_id": null,
+        "merge_when_pipeline_succeeds": false,
+        "milestone_id": null,
+        "source_branch": "test",
+        "source_project_id": 30456730,
+        "state_id": 1,
+        "target_branch": "main",
+        "target_project_id": 30456730,
+        "time_estimate": 0,
+        "title": "Test MR",
+        "updated_at": "2021-11-17 12:46:01 UTC",
+        "updated_by_id": 3612771,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1",
+        "source": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "target": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "last_commit": {
+            "id": "fcad4dba40a75eb106df49e90dd7eb162ac95589",
+            "message": "Add new commit\n",
+            "title": "Add new commit",
+            "timestamp": "2021-11-07T18:19:26+00:00",
+            "url": "https://gitlab.com/FabioRosado/test-project/-/commit/fcad4dba40a75eb106df49e90dd7eb162ac95589",
+            "author": {
+                "name": "FabioRosado",
+                "email": "fabioglrosado@gmail.com"
+            }
+        },
+        "work_in_progress": false,
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [
+            3612771
+        ],
+        "state": "opened",
+        "action": "approved"
+    },
+    "labels": [
+        {
+            "id": 22478750,
+            "title": "blah",
+            "color": "#ff0000",
+            "project_id": 30456730,
+            "created_at": "2021-11-07 18:17:22 UTC",
+            "updated_at": "2021-11-07 18:17:22 UTC",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {
+        "updated_at": {
+            "previous": "2021-11-17 12:26:56 UTC",
+            "current": "2021-11-17 12:46:01 UTC"
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    },
+    "assignees": [
+        {
+            "id": 3612771,
+            "name": "Fabio Rosado",
+            "username": "FabioRosado",
+            "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+            "email": "[REDACTED]"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_closed.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_closed.json
new file mode 100644
index 000000000..95a34f47f
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_closed.json
@@ -0,0 +1,132 @@
+{
+    "object_kind": "merge_request",
+    "event_type": "merge_request",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https: //secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "assignee_id": null,
+        "author_id": 3612771,
+        "created_at": "2021-11-17 12:57:16 UTC",
+        "description": "This reverts merge request !1",
+        "head_pipeline_id": null,
+        "id": 126521680,
+        "iid": 2,
+        "last_edited_at": null,
+        "last_edited_by_id": null,
+        "merge_commit_sha": null,
+        "merge_error": null,
+        "merge_params": {
+            "force_remove_source_branch": "1"
+        },
+        "merge_status": "can_be_merged",
+        "merge_user_id": null,
+        "merge_when_pipeline_succeeds": false,
+        "milestone_id": null,
+        "source_branch": "revert-5e25ad25",
+        "source_project_id": 30456730,
+        "state_id": 2,
+        "target_branch": "main",
+        "target_project_id": 30456730,
+        "time_estimate": 0,
+        "title": "Revert Merge branch \"test\" into \"main\"",
+        "updated_at": "2021-11-17 12:58:12 UTC",
+        "updated_by_id": null,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/merge_requests/2",
+        "source": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "target": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "last_commit": {
+            "id": "ef91bab5bae1d4fac5c9cb33472188bbdfbe2757",
+            "message": "Revert Merge branch \"test\" into \"main\"\n\nThis reverts merge request !1",
+            "title": "Revert Merge branch \"test\" into \"main\"",
+            "timestamp": "2021-11-17T12:57:09+00:00",
+            "url": "https://gitlab.com/FabioRosado/test-project/-/commit/ef91bab5bae1d4fac5c9cb33472188bbdfbe2757",
+            "author": {
+                "name": "Fabio Rosado",
+                "email": "fabioglrosado@gmail.com"
+            }
+        },
+        "work_in_progress": false,
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [],
+        "state": "closed",
+        "action": "close"
+    },
+    "labels": [],
+    "changes": {
+        "state_id": {
+            "previous": 1,
+            "current": 2
+        },
+        "updated_at": {
+            "previous": "2021-11-17 12:57:16 UTC",
+            "current": "2021-11-17 12:58:12 UTC"
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    }
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_label_update.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_label_update.json
new file mode 100644
index 000000000..476480ff9
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_label_update.json
@@ -0,0 +1,171 @@
+{
+        "object_kind": "merge_request",
+        "event_type": "merge_request",
+        "user": {
+            "id": 3612771,
+            "name": "Fabio Rosado",
+            "username": "FabioRosado",
+            "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+            "email": "[REDACTED]"
+        },
+        "project": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "object_attributes": {
+            "assignee_id": 3612771,
+            "author_id": 3612771,
+            "created_at": "2021-11-07 18:14:50 UTC",
+            "description": "",
+            "head_pipeline_id": null,
+            "id": 124957728,
+            "iid": 1,
+            "last_edited_at": null,
+            "last_edited_by_id": null,
+            "merge_commit_sha": null,
+            "merge_error": null,
+            "merge_params": {"force_remove_source_branch": "1"},
+            "merge_status": "can_be_merged",
+            "merge_user_id": null,
+            "merge_when_pipeline_succeeds": false,
+            "milestone_id": null,
+            "source_branch": "test",
+            "source_project_id": 30456730,
+            "state_id": 1,
+            "target_branch": "main",
+            "target_project_id": 30456730,
+            "time_estimate": 0,
+            "title": "Test MR",
+            "updated_at": "2021-11-07 18:17:29 UTC",
+            "updated_by_id": 3612771,
+            "url": "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1",
+            "source": {
+                "id": 30456730,
+                "name": "test-project",
+                "description": "",
+                "web_url": "https://gitlab.com/FabioRosado/test-project",
+                "avatar_url": null,
+                "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+                "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+                "namespace": "Fabio Rosado",
+                "visibility_level": 0,
+                "path_with_namespace": "FabioRosado/test-project",
+                "default_branch": "main",
+                "ci_config_path": "",
+                "homepage": "https://gitlab.com/FabioRosado/test-project",
+                "url": "git@gitlab.com:FabioRosado/test-project.git",
+                "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+                "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+            },
+            "target": {
+                "id": 30456730,
+                "name": "test-project",
+                "description": "",
+                "web_url": "https://gitlab.com/FabioRosado/test-project",
+                "avatar_url": null,
+                "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+                "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+                "namespace": "Fabio Rosado",
+                "visibility_level": 0,
+                "path_with_namespace": "FabioRosado/test-project",
+                "default_branch": "main",
+                "ci_config_path": "",
+                "homepage": "https://gitlab.com/FabioRosado/test-project",
+                "url": "git@gitlab.com:FabioRosado/test-project.git",
+                "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+                "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+            },
+            "last_commit": {
+                "id": "32117d8dffa9c0788c7a1842df1333772dc6aaf6",
+                "message": "Add test\n",
+                "title": "Add test",
+                "timestamp": "2021-11-07T18:14:28+00:00",
+                "url": "https://gitlab.com/FabioRosado/test-project/-/commit/32117d8dffa9c0788c7a1842df1333772dc6aaf6",
+                "author": {"name": "FabioRosado", "email": "fabioglrosado@gmail.com"}
+            },
+            "work_in_progress": false,
+            "total_time_spent": 0,
+            "time_change": 0,
+            "human_total_time_spent": null,
+            "human_time_change": null,
+            "human_time_estimate": null,
+            "assignee_ids": [3612771],
+            "state": "opened",
+            "action": "update"
+        },
+        "labels": [
+            {
+                "id": 22478750,
+                "title": "blah",
+                "color": "#ff0000",
+                "project_id": 30456730,
+                "created_at": "2021-11-07 18:17:22 UTC",
+                "updated_at": "2021-11-07 18:17:22 UTC",
+                "template": false,
+                "description": null,
+                "type": "ProjectLabel",
+                "group_id": null
+            }
+        ],
+        "changes": {
+            "labels": {
+                "previous": [
+                    {
+                        "id": 22478741,
+                        "title": "test-label",
+                        "color": "#009966",
+                        "project_id": 30456730,
+                        "created_at": "2021-11-07 18:10:43 UTC",
+                        "updated_at": "2021-11-07 18:10:43 UTC",
+                        "template": false,
+                        "description": null,
+                        "type": "ProjectLabel",
+                        "group_id": null
+                    }
+                ],
+                "current": [
+                    {
+                        "id": 22478750,
+                        "title": "blah",
+                        "color": "#ff0000",
+                        "project_id": 30456730,
+                        "created_at": "2021-11-07 18:17:22 UTC",
+                        "updated_at": "2021-11-07 18:17:22 UTC",
+                        "template": false,
+                        "description": null,
+                        "type": "ProjectLabel",
+                        "group_id": null
+                    }
+                ]
+            }
+        },
+        "repository": {
+            "name": "test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "description": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project"
+        },
+        "assignees": [
+            {
+                "id": 3612771,
+                "name": "Fabio Rosado",
+                "username": "FabioRosado",
+                "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+                "email": "[REDACTED]"
+            }
+        ]
+    }
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_labeled.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_labeled.json
new file mode 100644
index 000000000..bd9a42d2e
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_labeled.json
@@ -0,0 +1,163 @@
+{
+    "object_kind": "merge_request",
+    "event_type": "merge_request",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "assignee_id": 3612771,
+        "author_id": 3612771,
+        "created_at": "2021-11-07 18:14:50 UTC",
+        "description": "",
+        "head_pipeline_id": null,
+        "id": 124957728,
+        "iid": 1,
+        "last_edited_at": null,
+        "last_edited_by_id": null,
+        "merge_commit_sha": null,
+        "merge_error": null,
+        "merge_params": {"force_remove_source_branch": "1"},
+        "merge_status": "can_be_merged",
+        "merge_user_id": null,
+        "merge_when_pipeline_succeeds": false,
+        "milestone_id": null,
+        "source_branch": "test",
+        "source_project_id": 30456730,
+        "state_id": 1,
+        "target_branch": "main",
+        "target_project_id": 30456730,
+        "time_estimate": 0,
+        "title": "Test MR",
+        "updated_at": "2021-11-07 18:16:12 UTC",
+        "updated_by_id": 3612771,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1",
+        "source": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "target": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "last_commit": {
+            "id": "32117d8dffa9c0788c7a1842df1333772dc6aaf6",
+            "message": "Add test\n",
+            "title": "Add test",
+            "timestamp": "2021-11-07T18:14:28+00:00",
+            "url": "https://gitlab.com/FabioRosado/test-project/-/commit/32117d8dffa9c0788c7a1842df1333772dc6aaf6",
+            "author": {"name": "FabioRosado", "email": "fabioglrosado@gmail.com"}
+        },
+        "work_in_progress": false,
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [3612771],
+        "state": "opened",
+        "action": "update"
+    },
+    "labels": [
+        {
+            "id": 22478741,
+            "title": "test-label",
+            "color": "#009966",
+            "project_id": 30456730,
+            "created_at": "2021-11-07 18:10:43 UTC",
+            "updated_at": "2021-11-07 18:10:43 UTC",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {
+        "updated_at": {
+            "previous": "2021-11-07 18:16:12 UTC",
+            "current": "2021-11-07 18:16:12 UTC"
+        },
+        "updated_by_id": {"previous": null, "current": 3612771},
+        "labels": {
+            "previous": [],
+            "current": [
+                {
+                    "id": 22478741,
+                    "title": "test-label",
+                    "color": "#009966",
+                    "project_id": 30456730,
+                    "created_at": "2021-11-07 18:10:43 UTC",
+                    "updated_at": "2021-11-07 18:10:43 UTC",
+                    "template": false,
+                    "description": null,
+                    "type": "ProjectLabel",
+                    "group_id": null
+                }
+            ]
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    },
+    "assignees": [
+        {
+            "id": 3612771,
+            "name": "Fabio Rosado",
+            "username": "FabioRosado",
+            "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+            "email": "[REDACTED]"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_merged.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_merged.json
new file mode 100644
index 000000000..090747aac
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_merged.json
@@ -0,0 +1,156 @@
+{
+    "object_kind": "merge_request",
+    "event_type": "merge_request",
+    "user": {
+        "id": 3612771,
+        "name": "Fabio Rosado",
+        "username": "FabioRosado",
+        "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+        "email": "[REDACTED]"
+    },
+    "project": {
+        "id": 30456730,
+        "name": "test-project",
+        "description": "",
+        "web_url": "https://gitlab.com/FabioRosado/test-project",
+        "avatar_url": null,
+        "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+        "namespace": "Fabio Rosado",
+        "visibility_level": 0,
+        "path_with_namespace": "FabioRosado/test-project",
+        "default_branch": "main",
+        "ci_config_path": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+        "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+    },
+    "object_attributes": {
+        "assignee_id": 3612771,
+        "author_id": 3612771,
+        "created_at": "2021-11-07 18:14:50 UTC",
+        "description": "",
+        "head_pipeline_id": null,
+        "id": 124957728,
+        "iid": 1,
+        "last_edited_at": null,
+        "last_edited_by_id": null,
+        "merge_commit_sha": "5e25ad25ca9133fdfaa5263db83dc4ca3024b589",
+        "merge_error": null,
+        "merge_params": {
+            "force_remove_source_branch": "1"
+        },
+        "merge_status": "can_be_merged",
+        "merge_user_id": null,
+        "merge_when_pipeline_succeeds": false,
+        "milestone_id": null,
+        "source_branch": "test",
+        "source_project_id": 30456730,
+        "state_id": 3,
+        "target_branch": "main",
+        "target_project_id": 30456730,
+        "time_estimate": 0,
+        "title": "Test MR",
+        "updated_at": "2021-11-17 12:53:22 UTC",
+        "updated_by_id": 3612771,
+        "url": "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1",
+        "source": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "target": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "last_commit": {
+            "id": "fcad4dba40a75eb106df49e90dd7eb162ac95589",
+            "message": "Add new commit\n",
+            "title": "Add new commit",
+            "timestamp": "2021-11-07T18:19:26+00:00",
+            "url": "https://gitlab.com/FabioRosado/test-project/-/commit/fcad4dba40a75eb106df49e90dd7eb162ac95589",
+            "author": {
+                "name": "FabioRosado",
+                "email": "fabioglrosado@gmail.com"
+            }
+        },
+        "work_in_progress": false,
+        "total_time_spent": 0,
+        "time_change": 0,
+        "human_total_time_spent": null,
+        "human_time_change": null,
+        "human_time_estimate": null,
+        "assignee_ids": [
+            3612771
+        ],
+        "state": "merged",
+        "action": "merge"
+    },
+    "labels": [
+        {
+            "id": 22478750,
+            "title": "blah",
+            "color": "#ff0000",
+            "project_id": 30456730,
+            "created_at": "2021-11-07 18:17:22 UTC",
+            "updated_at": "2021-11-07 18:17:22 UTC",
+            "template": false,
+            "description": null,
+            "type": "ProjectLabel",
+            "group_id": null
+        }
+    ],
+    "changes": {
+        "state_id": {
+            "previous": 4,
+            "current": 3
+        },
+        "updated_at": {
+            "previous": "2021-11-17 12:53:22 UTC",
+            "current": "2021-11-17 12:53:22 UTC"
+        }
+    },
+    "repository": {
+        "name": "test-project",
+        "url": "git@gitlab.com:FabioRosado/test-project.git",
+        "description": "",
+        "homepage": "https://gitlab.com/FabioRosado/test-project"
+    },
+    "assignees": [
+        {
+            "id": 3612771,
+            "name": "Fabio Rosado",
+            "username": "FabioRosado",
+            "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+            "email": "[REDACTED]"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_opened.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_opened.json
new file mode 100644
index 000000000..ac6d12f81
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/mr_opened.json
@@ -0,0 +1,126 @@
+{
+ "object_kind": "merge_request",
+        "event_type": "merge_request",
+        "user": {
+            "id": 3612771,
+            "name": "Fabio Rosado",
+            "username": "FabioRosado",
+            "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+            "email": "[REDACTED]"
+        },
+        "project": {
+            "id": 30456730,
+            "name": "test-project",
+            "description": "",
+            "web_url": "https://gitlab.com/FabioRosado/test-project",
+            "avatar_url": null,
+            "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+            "namespace": "Fabio Rosado",
+            "visibility_level": 0,
+            "path_with_namespace": "FabioRosado/test-project",
+            "default_branch": "main",
+            "ci_config_path": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+            "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+        },
+        "object_attributes": {
+            "assignee_id": 3612771,
+            "author_id": 3612771,
+            "created_at": "2021-11-07T18:14:50.582Z",
+            "description": "",
+            "head_pipeline_id": null,
+            "id": 124957728,
+            "iid": 1,
+            "last_edited_at": null,
+            "last_edited_by_id": null,
+            "merge_commit_sha": null,
+            "merge_error": null,
+            "merge_params": {"force_remove_source_branch": "1"},
+            "merge_status": "can_be_merged",
+            "merge_user_id": null,
+            "merge_when_pipeline_succeeds": false,
+            "milestone_id": null,
+            "source_branch": "test",
+            "source_project_id": 30456730,
+            "state_id": 1,
+            "target_branch": "main",
+            "target_project_id": 30456730,
+            "time_estimate": 0,
+            "title": "Test MR",
+            "updated_at": "2021-11-07T18:14:50.582Z",
+            "updated_by_id": null,
+            "url": "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1",
+            "source": {
+                "id": 30456730,
+                "name": "test-project",
+                "description": "",
+                "web_url": "https://gitlab.com/FabioRosado/test-project",
+                "avatar_url": null,
+                "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+                "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+                "namespace": "Fabio Rosado",
+                "visibility_level": 0,
+                "path_with_namespace": "FabioRosado/test-project",
+                "default_branch": "main",
+                "ci_config_path": "",
+                "homepage": "https://gitlab.com/FabioRosado/test-project",
+                "url": "git@gitlab.com:FabioRosado/test-project.git",
+                "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+                "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+            },
+            "target": {
+                "id": 30456730,
+                "name": "test-project",
+                "description": "",
+                "web_url": "https://gitlab.com/FabioRosado/test-project",
+                "avatar_url": null,
+                "git_ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+                "git_http_url": "https://gitlab.com/FabioRosado/test-project.git",
+                "namespace": "Fabio Rosado",
+                "visibility_level": 0,
+                "path_with_namespace": "FabioRosado/test-project",
+                "default_branch": "main",
+                "ci_config_path": "",
+                "homepage": "https://gitlab.com/FabioRosado/test-project",
+                "url": "git@gitlab.com:FabioRosado/test-project.git",
+                "ssh_url": "git@gitlab.com:FabioRosado/test-project.git",
+                "http_url": "https://gitlab.com/FabioRosado/test-project.git"
+            },
+            "last_commit": {
+                "id": "32117d8dffa9c0788c7a1842df1333772dc6aaf6",
+                "message": "Add test\n",
+                "title": "Add test",
+                "timestamp": "2021-11-07T18:14:28+00:00",
+                "url": "https://gitlab.com/FabioRosado/test-project/-/commit/32117d8dffa9c0788c7a1842df1333772dc6aaf6",
+                "author": {"name": "FabioRosado", "email": "fabioglrosado@gmail.com"}
+            },
+            "work_in_progress": false,
+            "total_time_spent": 0,
+            "time_change": 0,
+            "human_total_time_spent": null,
+            "human_time_change": null,
+            "human_time_estimate": null,
+            "assignee_ids": [3612771],
+            "state": "opened"
+        },
+        "labels": [],
+        "changes": {},
+        "repository": {
+            "name": "test-project",
+            "url": "git@gitlab.com:FabioRosado/test-project.git",
+            "description": "",
+            "homepage": "https://gitlab.com/FabioRosado/test-project"
+        },
+        "assignees": [
+            {
+                "id": 3612771,
+                "name": "Fabio Rosado",
+                "username": "FabioRosado",
+                "avatar_url": "https://secure.gravatar.com/avatar/c13aab50cd7f21b8c340136ac4ce515a?s=80&d=identicon",
+                "email": "[REDACTED]"
+            }
+        ]
+    }
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/gitlab_response_payloads/push.json b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/push.json
new file mode 100644
index 000000000..5dbaff8fe
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/gitlab_response_payloads/push.json
@@ -0,0 +1,71 @@
+{
+    "object_kind": "push",
+    "event_name": "push",
+    "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+    "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+    "ref": "refs/heads/master",
+    "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+    "user_id": 4,
+    "user_name": "John Smith",
+    "user_username": "jsmith",
+    "user_email": "john@example.com",
+    "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+    "project_id": 15,
+    "project":{
+      "id": 15,
+      "name":"Diaspora",
+      "description":"",
+      "web_url":"http://example.com/mike/diaspora",
+      "avatar_url":null,
+      "git_ssh_url":"git@example.com:mike/diaspora.git",
+      "git_http_url":"http://example.com/mike/diaspora.git",
+      "namespace":"Mike",
+      "visibility_level":0,
+      "path_with_namespace":"mike/diaspora",
+      "default_branch":"master",
+      "homepage":"http://example.com/mike/diaspora",
+      "url":"git@example.com:mike/diaspora.git",
+      "ssh_url":"git@example.com:mike/diaspora.git",
+      "http_url":"http://example.com/mike/diaspora.git"
+    },
+    "repository":{
+      "name": "Diaspora",
+      "url": "git@example.com:mike/diaspora.git",
+      "description": "",
+      "homepage": "http://example.com/mike/diaspora",
+      "git_http_url":"http://example.com/mike/diaspora.git",
+      "git_ssh_url":"git@example.com:mike/diaspora.git",
+      "visibility_level":0
+    },
+    "commits": [
+      {
+        "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+        "message": "Update Catalan translation to e38cb41.\n\nSee https://gitlab.com/gitlab-org/gitlab for more information",
+        "title": "Update Catalan translation to e38cb41.",
+        "timestamp": "2011-12-12T14:27:31+02:00",
+        "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+        "author": {
+          "name": "Jordi Mallach",
+          "email": "jordi@softcatala.org"
+        },
+        "added": ["CHANGELOG"],
+        "modified": ["app/controller/application.rb"],
+        "removed": []
+      },
+      {
+        "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+        "message": "fixed readme",
+        "title": "fixed readme",
+        "timestamp": "2012-01-03T23:36:29+02:00",
+        "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+        "author": {
+          "name": "GitLab dev user",
+          "email": "gitlabdev@dv6700.(none)"
+        },
+        "added": ["CHANGELOG"],
+        "modified": ["app/controller/application.rb"],
+        "removed": []
+      }
+    ],
+    "total_commits_count": 4
+  }
\ No newline at end of file
diff --git a/opsdroid/connector/gitlab/tests/test_connector_gitlab.py b/opsdroid/connector/gitlab/tests/test_connector_gitlab.py
new file mode 100644
index 000000000..8c5e9e131
--- /dev/null
+++ b/opsdroid/connector/gitlab/tests/test_connector_gitlab.py
@@ -0,0 +1,746 @@
+import logging
+import asyncio
+from pathlib import Path
+
+import asynctest.mock as amock
+import pytest
+
+import opsdroid.connector.gitlab.events as gitlab_events
+from opsdroid.connector.gitlab import ConnectorGitlab
+from opsdroid.matchers import match_event
+from opsdroid.testing import call_endpoint, running_opsdroid
+from opsdroid.const import GITLAB_API_ENDPOINT
+from opsdroid.events import Message
+
+
+@pytest.fixture
+async def connector(opsdroid, mock_api_obj):
+    opsdroid.config["connectors"] = {"gitlab": {"webhook-token": "secret-stuff!"}}
+    opsdroid.config["web"] = {"base-url": mock_api_obj.base_url}
+
+    await opsdroid.load()
+    return opsdroid.get_connector("gitlab")
+
+
+def get_response_path(response: str) -> Path:
+    return Path(__file__).parent / "gitlab_response_payloads" / response
+
+
+def get_webhook_payload(path: str) -> str:
+    with open(get_response_path(path), "r") as fh:
+        return fh.read()
+
+
+def test_app_init():
+    """Test that the connector is initialised properly when using Gitlab"""
+    connector = ConnectorGitlab({"name": "gitlab", "webhook-token": "secret-stuff!"})
+    assert connector.name == "gitlab"
+    assert connector.webhook_token == "secret-stuff!"
+
+
+def test_init(opsdroid):
+    connector = ConnectorGitlab({}, opsdroid=opsdroid)
+    assert connector.name == "gitlab"
+    assert connector.opsdroid == opsdroid
+    assert connector.base_url is None
+
+
+def test_optional_config(opsdroid):
+    config = {
+        "name": "my-gitlab",
+        "forward-url": "http://my-awesome-url",
+        "webhook-token": "secret-stuff",
+    }
+
+    connector = ConnectorGitlab(config, opsdroid)
+    assert connector.name == "my-gitlab"
+    assert connector.base_url == "http://my-awesome-url"
+    assert connector.webhook_token == "secret-stuff"
+
+
+def test_base_url(opsdroid):
+    opsdroid.config["web"] = {"base-url": "http://example.com"}
+
+    connector = ConnectorGitlab({}, opsdroid)
+
+    assert connector.base_url == "http://example.com"
+
+
+@pytest.mark.asyncio
+async def test_gitlab_webhook_handler_excepion(caplog):
+    caplog.set_level(logging.DEBUG)
+    connector = ConnectorGitlab({"name": "gitlab"})
+    mocked_request = amock.CoroutineMock()
+    mocked_request.json.side_effect = Exception()
+
+    resp = await connector.gitlab_webhook_handler(mocked_request)
+
+    assert resp.status == 400
+    assert "Unable to get JSON from request" in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_validate_request(opsdroid):
+    config = {"webhook-token": "secret-stuff"}
+    connector = ConnectorGitlab(config, opsdroid)
+
+    request = amock.CoroutineMock()
+    request.headers = {"X-Gitlab-Token": "secret-stuff"}
+
+    is_valid = await connector.validate_request(request)
+
+    assert is_valid
+
+    fake_request = amock.CoroutineMock()
+    request.headers = {}
+
+    is_valid = await connector.validate_request(fake_request)
+
+    assert not is_valid
+
+
+@pytest.mark.asyncio
+async def test_listen(connector):
+    assert await connector.listen() is None
+
+
+@pytest.mark.asyncio
+async def test_issue_created(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GitlabIssueCreated)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/issues/1"
+        target = f"{GITLAB_API_ENDPOINT}/projects/30456730/issues/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.target == target
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "New test issue"
+        assert event.description == "Test description"
+        assert event.labels == ["test-label"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("issue_created.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_issue_label_updated(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GitlabIssueLabeled)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/issues/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "New test issue"
+        assert event.description == "This should have been filled"
+        assert event.labels == ["test-label"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("issue_label_update.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_issue_labeled(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GitlabIssueLabeled)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/issues/2"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "test"
+        assert event.description == ""
+        assert event.labels == ["blah"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("issue_labeled.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_issue_edited(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GitlabIssueEdited)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/issues/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "New test issue"
+        assert event.description == "This should have been filled"
+        assert event.labels == ["test-label"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("issue_message_edited.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_generic_issue(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GenericIssueEvent)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/issues/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "New test issue"
+        assert event.description == "This should have been filled"
+        assert event.labels == ["test-label"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("generic_issue.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_no_token_returns_401(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GitlabIssueCreated)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/issues/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "New test issue"
+        assert event.description == "Test description"
+        assert event.labels == ["test-label"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("issue_created.json"),
+        )
+
+        assert resp.status == 401
+        assert "Test skill complete" not in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_issue_closed(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GitlabIssueClosed)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/issues/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "New test issue"
+        assert event.description == "This should have been filled"
+        assert event.labels == ["test-label"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("issue_closed.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_generic_issue_event(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GenericGitlabEvent)
+    async def test_skill(opsdroid, config, event):
+        url = "http://example.com/mike/diaspora"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "Diaspora"
+        assert event.user == "jsmith"
+        assert event.title is None
+        assert event.description is None
+        assert event.labels == []
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("push.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_bad_json_file(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.DEBUG)
+
+    @match_event(gitlab_events.GenericGitlabEvent)
+    async def test_skill(opsdroid, config, event):
+        url = "http://example.com/mike/diaspora"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "Diaspora"
+        assert event.user == "jsmith"
+        assert event.title is None
+        assert event.description is None
+        assert event.labels == []
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("bad_json.json"),
+        )
+
+        assert resp.status == 400
+        assert "Unable to decode json" in caplog.text
+        assert "Test skill complete" not in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_mr_label_update_event(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.MRLabeled)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "Test MR"
+        assert event.description == ""
+        assert event.labels == ["blah"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("mr_label_update.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_mr_opened_event(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.MRCreated)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "Test MR"
+        assert event.description == ""
+        assert event.labels == []
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("mr_opened.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_mr_merged_event(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.MRMerged)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "Test MR"
+        assert event.description == ""
+        assert event.labels == ["blah"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("mr_merged.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_mr_approved_event(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.MRApproved)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/merge_requests/1"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == "Test MR"
+        assert event.description == ""
+        assert event.labels == ["blah"]
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("mr_approved.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_mr_closed_event(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.MRClosed)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/merge_requests/2"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == 'Revert Merge branch "test" into "main"'
+        assert event.description == "This reverts merge request !1"
+        assert event.labels == []
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("mr_closed.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_mr_generic_event(opsdroid, connector, mock_api, caplog):
+    caplog.set_level(logging.INFO)
+
+    @match_event(gitlab_events.GenericMREvent)
+    async def test_skill(opsdroid, config, event):
+        url = "https://gitlab.com/FabioRosado/test-project/-/merge_requests/2"
+        assert event.connector.name == "gitlab"
+        assert event.url == url
+        assert event.project == "test-project"
+        assert event.user == "FabioRosado"
+        assert event.title == 'Revert Merge branch "test" into "main"'
+        assert event.description == "This reverts merge request !1"
+        assert event.labels == []
+        logging.getLogger(__name__).info("Test skill complete")
+
+    opsdroid.register_skill(test_skill, config={"name": "test"})
+
+    async with running_opsdroid(opsdroid):
+        resp = await call_endpoint(
+            opsdroid,
+            "/connector/gitlab",
+            "POST",
+            headers={
+                "X-Gitlab-Token": "secret-stuff!",
+                "Content-Type": "application/json",
+            },
+            data=get_webhook_payload("generic_mr.json"),
+        )
+
+        assert resp.status == 200
+        assert "Test skill complete" in caplog.text
+        assert "Exception when running skill" not in caplog.text
+
+
+ISSUE_TARGET = "FabioRosado/test-project/-/issues/1"
+
+
+@pytest.mark.asyncio
+async def test_send_message(opsdroid, caplog):
+    caplog.set_level(logging.DEBUG)
+
+    connector = ConnectorGitlab(
+        {"webhook-token": "secret-stuff!", "token": "my-token"},
+        opsdroid=opsdroid,
+    )
+
+    response = amock.Mock()
+    response.status = 201
+
+    with amock.patch(
+        "aiohttp.ClientSession.post", new=amock.CoroutineMock()
+    ) as patched_request:
+        patched_request.return_value = asyncio.Future()
+        patched_request.return_value.set_result(response)
+
+        assert opsdroid.__class__.instances
+
+        test_message = Message(
+            text="This is a test",
+            user="opsdroid",
+            target=ISSUE_TARGET,
+            connector=connector,
+        )
+
+        patched_request.return_value = asyncio.Future()
+        patched_request.return_value.set_result(response)
+
+        result = await connector.send(test_message)
+
+        assert patched_request.called
+        assert "Responding via Gitlab" in caplog.text
+        assert "Message 'This is a test' sent to GitLab" in caplog.text
+        assert result is True
+
+
+@pytest.mark.asyncio
+async def test_send_message_bad_status(opsdroid, caplog):
+    caplog.set_level(logging.DEBUG)
+
+    connector = ConnectorGitlab(
+        {"webhook-token": "secret-stuff!", "token": "my-token"},
+        opsdroid=opsdroid,
+    )
+
+    response = amock.Mock()
+    response.status = 422
+
+    with amock.patch(
+        "aiohttp.ClientSession.post", new=amock.CoroutineMock()
+    ) as patched_request:
+        patched_request.return_value = asyncio.Future()
+        patched_request.return_value.set_result(response)
+
+        assert opsdroid.__class__.instances
+
+        test_message = Message(
+            text="This is a test",
+            user="opsdroid",
+            target=ISSUE_TARGET,
+            connector=connector,
+        )
+
+        patched_request.return_value = asyncio.Future()
+        patched_request.return_value.set_result(response)
+
+        result = await connector.send(test_message)
+
+        assert patched_request.called
+        assert "Responding via Gitlab" in caplog.text
+        assert "Unable to send 'This is a test' to GitLab." in caplog.text
+        assert result is False
+
+
+@pytest.mark.asyncio
+async def test_send_message_no_token(opsdroid, caplog):
+    caplog.set_level(logging.DEBUG)
+
+    connector = ConnectorGitlab(
+        {"webhook-token": "secret-stuff!"},
+        opsdroid=opsdroid,
+    )
+
+    response = amock.Mock()
+
+    with amock.patch(
+        "aiohttp.ClientSession.post", new=amock.CoroutineMock()
+    ) as patched_request:
+        patched_request.return_value = asyncio.Future()
+        patched_request.return_value.set_result(response)
+
+        assert opsdroid.__class__.instances
+
+        test_message = Message(
+            text="This is a test",
+            user="opsdroid",
+            target=ISSUE_TARGET,
+            connector=connector,
+        )
+
+        patched_request.return_value = asyncio.Future()
+        patched_request.return_value.set_result(response)
+
+        result = await connector.send(test_message)
+
+        assert not patched_request.called
+        assert "Unable to reply to GitLab" in caplog.text
+        assert result is False
diff --git a/opsdroid/testing/utils.py b/opsdroid/testing/utils.py
index 298d8a0ab..a44346cfc 100644
--- a/opsdroid/testing/utils.py
+++ b/opsdroid/testing/utils.py
@@ -3,8 +3,8 @@
 import asyncio
 import json
 import time
-from typing import Any, Awaitable, Dict, List
 from contextlib import asynccontextmanager
+from typing import Any, Awaitable, Dict, List, Union
 
 import aiohttp
 from aiohttp import web
@@ -98,7 +98,7 @@ async def call_endpoint(
     endpoint: str,
     method: str = "GET",
     data_path: str = None,
-    data: Dict = None,
+    data: Union[Dict, str] = None,
     headers: Dict = None,
     **kwargs: Any,
 ) -> web.Response: