Repository: port-labs/ocean
Pull request #1500: [Integration][Bitbucket] Add Support For Live Events
Author: login: mk-armah
Labels: name: size/XL
Created: 2025-03-18T16:18:04Z | Merged: 2025-03-24T19:47:57Z

# Description

What - This Pull Request adds support for Live events to bitbucket integration

Why - Live events support is essential to ensure real time data processing

How - Listens to Pull Requests and Repository incoming webhooks from bitbucket, processes them and ingests to Port in real time.
## Type of change

Please leave one option from the following and delete the rest:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] New Integration (non-breaking change which adds a new integration)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Non-breaking change (fix of existing functionality that will not change current behavior)
- [ ] Documentation (added/updated documentation)

<h4> All tests should be run against the port production environment(using a testing org). </h4>

### Core testing checklist

- [ ] Integration able to create all default resources from scratch
- [ ] Resync finishes successfully
- [ ] Resync able to create entities
- [ ] Resync able to update entities
- [ ] Resync able to detect and delete entities
- [ ] Scheduled resync able to abort existing resync and start a new one
- [ ] Tested with at least 2 integrations from scratch
- [ ] Tested with Kafka and Polling event listeners
- [ ] Tested deletion of entities that don't pass the selector


### Integration testing checklist

- [ ] Integration able to create all default resources from scratch
- [ ] Resync able to create entities
- [ ] Resync able to update entities
- [ ] Resync able to detect and delete entities
- [ ] Resync finishes successfully
- [ ] If new resource kind is added or updated in the integration, add example raw data, mapping and expected result to the `examples` folder in the integration directory.
- [ ] If resource kind is updated, run the integration with the example data and check if the expected result is achieved
- [ ] If new resource kind is added or updated, validate that live-events for that resource are working as expected
- [ ] Docs PR link [here](#)

### Preflight checklist

- [ ] Handled rate limiting
- [ ] Handled pagination
- [ ] Implemented the code in async
- [ ] Support Multi account

## Screenshots

Include screenshots from your environment showing how the resources of the integration will look.

## API Documentation

Provide links to the API documentation used for this integration.



Diff:
diff --git a/integrations/bitbucket-cloud/.port/spec.yaml b/integrations/bitbucket-cloud/.port/spec.yaml
index de6754fcae..bcb608cebb 100644
--- a/integrations/bitbucket-cloud/.port/spec.yaml
+++ b/integrations/bitbucket-cloud/.port/spec.yaml
@@ -30,3 +30,8 @@ configurations:
     type: string
     description: The base api url of Bitbucket Cloud Rest API. It should include the protocol and version (e.g. <a href="https://api.bitbucket.org/2.0" target="_blank">https://api.bitbucket.org/2.0</a>).
     default: https://api.bitbucket.org/2.0
+  - name: webhookSecret
+    type: string
+    description: The secret used to verify the webhook requests.
+    sensitive: true
+    required: false
diff --git a/integrations/bitbucket-cloud/CHANGELOG.md b/integrations/bitbucket-cloud/CHANGELOG.md
index a44ec5d246..0d5af0fabc 100644
--- a/integrations/bitbucket-cloud/CHANGELOG.md
+++ b/integrations/bitbucket-cloud/CHANGELOG.md
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 <!-- towncrier release notes start -->
 
+## 0.1.3-dev (2025-03-24)
+
+
+### Features
+
+- Added support for live events
+
+
 ## 0.1.2-dev (2025-03-24)
 
 
diff --git a/integrations/bitbucket-cloud/helpers/__init__.py b/integrations/bitbucket-cloud/bitbucket_cloud/__init__.py
similarity index 100%
rename from integrations/bitbucket-cloud/helpers/__init__.py
rename to integrations/bitbucket-cloud/bitbucket_cloud/__init__.py
diff --git a/integrations/bitbucket-cloud/client.py b/integrations/bitbucket-cloud/bitbucket_cloud/client.py
similarity index 90%
rename from integrations/bitbucket-cloud/client.py
rename to integrations/bitbucket-cloud/bitbucket_cloud/client.py
index 20c9f11fe8..c236f248cf 100644
--- a/integrations/bitbucket-cloud/client.py
+++ b/integrations/bitbucket-cloud/bitbucket_cloud/client.py
@@ -2,11 +2,11 @@
 from httpx import HTTPError, HTTPStatusError
 from loguru import logger
 from port_ocean.utils import http_async_client
-from helpers.exceptions import MissingIntegrationCredentialException
+from bitbucket_cloud.helpers.exceptions import MissingIntegrationCredentialException
 from port_ocean.utils.cache import cache_iterator_result
 from port_ocean.context.ocean import ocean
-from helpers.rate_limiter import RollingWindowLimiter
-from helpers.utils import BitbucketRateLimiterConfig
+from bitbucket_cloud.helpers.rate_limiter import RollingWindowLimiter
+from bitbucket_cloud.helpers.utils import BitbucketRateLimiterConfig
 import base64
 
 PULL_REQUEST_STATE = "OPEN"
@@ -198,3 +198,17 @@ async def get_pull_requests(
                 f"Fetched batch of {len(pull_requests)} pull requests from repository {repo_slug} in workspace {self.workspace}"
             )
             yield pull_requests
+
+    async def get_pull_request(
+        self, repo_slug: str, pull_request_id: str
+    ) -> dict[str, Any]:
+        """Get a specific pull request by ID."""
+        return await self._send_api_request(
+            f"{self.base_url}/repositories/{self.workspace}/{repo_slug}/pullrequests/{pull_request_id}"
+        )
+
+    async def get_repository(self, repo_slug: str) -> dict[str, Any]:
+        """Get a specific repository by slug."""
+        return await self._send_api_request(
+            f"{self.base_url}/repositories/{self.workspace}/{repo_slug}"
+        )
diff --git a/integrations/bitbucket-cloud/bitbucket_cloud/helpers/__init__.py b/integrations/bitbucket-cloud/bitbucket_cloud/helpers/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/integrations/bitbucket-cloud/helpers/exceptions.py b/integrations/bitbucket-cloud/bitbucket_cloud/helpers/exceptions.py
similarity index 100%
rename from integrations/bitbucket-cloud/helpers/exceptions.py
rename to integrations/bitbucket-cloud/bitbucket_cloud/helpers/exceptions.py
diff --git a/integrations/bitbucket-cloud/helpers/folder.py b/integrations/bitbucket-cloud/bitbucket_cloud/helpers/folder.py
similarity index 95%
rename from integrations/bitbucket-cloud/helpers/folder.py
rename to integrations/bitbucket-cloud/bitbucket_cloud/helpers/folder.py
index 70126d617d..5777272ed8 100644
--- a/integrations/bitbucket-cloud/helpers/folder.py
+++ b/integrations/bitbucket-cloud/bitbucket_cloud/helpers/folder.py
@@ -1,12 +1,15 @@
 from collections import defaultdict
-from typing import Any, AsyncGenerator, Dict, List, Set, Tuple
+from typing import Any, AsyncGenerator, Dict, List, Set, Tuple, TYPE_CHECKING
 
 from loguru import logger
 import fnmatch
-from client import BitbucketClient
 from integration import FolderPattern
 
 
+if TYPE_CHECKING:
+    from bitbucket_cloud.client import BitbucketClient
+
+
 def extract_repo_names_from_patterns(
     folder_patterns: List[FolderPattern],
 ) -> Set[str]:
@@ -113,7 +116,7 @@ def find_common_base_path_and_max_depth(paths: List[str]) -> Tuple[str, int]:
 
 
 async def process_folder_patterns(
-    folder_patterns: list[FolderPattern], client: BitbucketClient
+    folder_patterns: list[FolderPattern], client: "BitbucketClient"
 ) -> AsyncGenerator[list[dict[str, Any]], None]:
     repo_names = extract_repo_names_from_patterns(folder_patterns)
     if not repo_names:
@@ -134,7 +137,7 @@ async def process_repo_folders(
     repo: dict[str, Any],
     pattern_by_repo: dict[str, dict[str, list[str]]],
     folder_patterns: list[FolderPattern],
-    client: BitbucketClient,
+    client: "BitbucketClient",
 ) -> AsyncGenerator[list[dict[str, Any]], None]:
     repo_name = repo["name"]
     if repo_name not in pattern_by_repo:
diff --git a/integrations/bitbucket-cloud/helpers/rate_limiter.py b/integrations/bitbucket-cloud/bitbucket_cloud/helpers/rate_limiter.py
similarity index 100%
rename from integrations/bitbucket-cloud/helpers/rate_limiter.py
rename to integrations/bitbucket-cloud/bitbucket_cloud/helpers/rate_limiter.py
diff --git a/integrations/bitbucket-cloud/helpers/utils.py b/integrations/bitbucket-cloud/bitbucket_cloud/helpers/utils.py
similarity index 100%
rename from integrations/bitbucket-cloud/helpers/utils.py
rename to integrations/bitbucket-cloud/bitbucket_cloud/helpers/utils.py
diff --git a/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/__init__.py b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/events.py b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/events.py
new file mode 100644
index 0000000000..a474575b6f
--- /dev/null
+++ b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/events.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+from enum import StrEnum, EnumMeta
+from typing import Any, Set, Type, Protocol, runtime_checkable
+from types import NotImplementedType
+
+
+@runtime_checkable
+class HasValues(Protocol):
+    @classmethod
+    def values(cls) -> Set[str]: ...
+
+
+class EventEnumMeta(EnumMeta):
+    """
+    Nothing fancy here, just a custom metaclass util that allows adding multiple enum classes at the class level to combine their unique values.
+    """
+
+    def __add__[
+        T: HasValues
+    ](cls: Type[T], other: Any) -> Set[str] | NotImplementedType:
+        if isinstance(other, type) and issubclass(other, HasValues):
+            return set(cls.values()) | set(other.values())
+        elif isinstance(other, set):
+            return set(cls.values()) | other
+        return NotImplemented
+
+    def __radd__[
+        T: HasValues
+    ](cls: Type[T], other: Any) -> Type[T] | Set[str] | NotImplementedType:
+        # Handle case where the class is on the right side of the addition
+        if isinstance(other, set):
+            return other | set(cls.values())
+        return NotImplemented
+
+
+class EventEnum(StrEnum, metaclass=EventEnumMeta):
+    """
+    Base event enum class that provides a class method to retrieve all member values.
+    """
+
+    @classmethod
+    def values(cls) -> set[str]:
+        return set(cls._value2member_map_.keys())
+
+
+class PullRequestEvents(EventEnum):
+    PULL_REQUEST_CREATED = "pullrequest:created"
+    PULL_REQUEST_UPDATED = "pullrequest:updated"
+    PULL_REQUEST_APPROVED = "pullrequest:approved"
+    PULL_REQUEST_UNAPPROVED = "pullrequest:unapproved"
+    PULL_REQUEST_FULFILLED = "pullrequest:fulfilled"
+    PULL_REQUEST_REJECTED = "pullrequest:rejected"
+
+
+class RepositoryEvents(EventEnum):
+    REPOSITORY_CREATED = "repo:created"
+    REPOSITORY_UPDATED = "repo:updated"
+    REPOSITORY_PUSHED = "repo:push"
diff --git a/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/__init__.py b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/_bitbucket_abstract_webhook_processor.py b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/_bitbucket_abstract_webhook_processor.py
new file mode 100644
index 0000000000..1c9eeffe06
--- /dev/null
+++ b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/_bitbucket_abstract_webhook_processor.py
@@ -0,0 +1,65 @@
+from port_ocean.core.handlers.webhook.abstract_webhook_processor import (
+    AbstractWebhookProcessor,
+)
+from initialize_client import init_webhook_client
+from port_ocean.core.handlers.webhook.webhook_event import (
+    EventHeaders,
+    EventPayload,
+    WebhookEvent,
+)
+from abc import abstractmethod
+
+import hashlib
+import hmac
+
+from fastapi import Request
+from loguru import logger
+
+
+class _BitbucketAbstractWebhookProcessor(AbstractWebhookProcessor):
+
+    _webhook_client = init_webhook_client()
+
+    async def authenticate(self, payload: EventPayload, headers: EventHeaders) -> bool:
+        return True
+
+    async def _verify_webhook_signature(self, request: Request) -> bool:
+        """Authenticate the Bitbucket webhook payload using the secret.
+        Skip if secret was not provided
+        """
+        if not self._webhook_client.secret:
+            logger.warning(
+                "No secret provided for authenticating incoming webhooks, skipping authentication."
+            )
+            return True
+
+        signature = request.headers.get("x-hub-signature")
+
+        if not signature:
+            logger.error(
+                "Aborting webhook authentication due to missing X-Hub-Signature header"
+            )
+            return False
+
+        body = await request.body()
+
+        hash_object = hmac.new(
+            self._webhook_client.secret.encode(), body, hashlib.sha256
+        )
+        expected_signature = "sha256=" + hash_object.hexdigest()
+
+        logger.debug(
+            "Webhook authentication: Comparing signatures",
+            received=signature,
+            expected=expected_signature,
+        )
+
+        return hmac.compare_digest(signature, expected_signature)
+
+    @abstractmethod
+    async def _should_process_event(self, event: WebhookEvent) -> bool: ...
+
+    async def should_process_event(self, event: WebhookEvent) -> bool:
+        if not (event._original_request and await self._should_process_event(event)):
+            return False
+        return await self._verify_webhook_signature(event._original_request)
diff --git a/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/pull_request_webhook_processor.py b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/pull_request_webhook_processor.py
new file mode 100644
index 0000000000..c9471961aa
--- /dev/null
+++ b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/pull_request_webhook_processor.py
@@ -0,0 +1,44 @@
+from loguru import logger
+from port_ocean.core.handlers.port_app_config.models import ResourceConfig
+from port_ocean.core.handlers.webhook.webhook_event import (
+    EventPayload,
+    WebhookEvent,
+    WebhookEventRawResults,
+)
+from bitbucket_cloud.webhook_processors.events import PullRequestEvents
+from bitbucket_cloud.helpers.utils import ObjectKind
+from bitbucket_cloud.webhook_processors.processors._bitbucket_abstract_webhook_processor import (
+    _BitbucketAbstractWebhookProcessor,
+)
+
+
+class PullRequestWebhookProcessor(_BitbucketAbstractWebhookProcessor):
+
+    async def _should_process_event(self, event: WebhookEvent) -> bool:
+        try:
+            return bool(PullRequestEvents(event.headers["x-event-key"]))
+        except ValueError:
+            return False
+
+    async def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
+        return [ObjectKind.PULL_REQUEST]
+
+    async def handle_event(
+        self, payload: EventPayload, resource_config: ResourceConfig
+    ) -> WebhookEventRawResults:
+        pull_request_id = payload["pullrequest"]["id"]
+        repository_id = payload["repository"]["uuid"]
+        logger.info(
+            f"Handling pull request webhook event for repository: {repository_id} and pull request: {pull_request_id}"
+        )
+        pull_request_details = await self._webhook_client.get_pull_request(
+            repository_id, pull_request_id
+        )
+        return WebhookEventRawResults(
+            updated_raw_results=[pull_request_details],
+            deleted_raw_results=[],
+        )
+
+    async def validate_payload(self, payload: EventPayload) -> bool:
+        required_fields = ["repository", "pullrequest"]
+        return all(field in payload for field in required_fields)
diff --git a/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/repository_webhook_processor.py b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/repository_webhook_processor.py
new file mode 100644
index 0000000000..34b21896ef
--- /dev/null
+++ b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/processors/repository_webhook_processor.py
@@ -0,0 +1,42 @@
+from port_ocean.core.handlers.port_app_config.models import ResourceConfig
+from port_ocean.core.handlers.webhook.webhook_event import (
+    EventPayload,
+    WebhookEvent,
+    WebhookEventRawResults,
+)
+from bitbucket_cloud.webhook_processors.events import RepositoryEvents
+from bitbucket_cloud.helpers.utils import ObjectKind
+from bitbucket_cloud.webhook_processors.processors._bitbucket_abstract_webhook_processor import (
+    _BitbucketAbstractWebhookProcessor,
+)
+from loguru import logger
+
+
+class RepositoryWebhookProcessor(_BitbucketAbstractWebhookProcessor):
+
+    async def _should_process_event(self, event: WebhookEvent) -> bool:
+        try:
+            return bool(RepositoryEvents(event.headers["x-event-key"]))
+        except ValueError:
+            return False
+
+    async def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
+        return [ObjectKind.REPOSITORY]
+
+    async def handle_event(
+        self, payload: EventPayload, resource_config: ResourceConfig
+    ) -> WebhookEventRawResults:
+
+        repository_id = payload["repository"]["uuid"]
+        logger.info(
+            f"Handling repository webhook event for repository: {repository_id}"
+        )
+        repository_details = await self._webhook_client.get_repository(repository_id)
+        return WebhookEventRawResults(
+            updated_raw_results=[repository_details],
+            deleted_raw_results=[],
+        )
+
+    async def validate_payload(self, payload: EventPayload) -> bool:
+        required_field = "repository"
+        return required_field in payload
diff --git a/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/webhook_client.py b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/webhook_client.py
new file mode 100644
index 0000000000..c109fe444f
--- /dev/null
+++ b/integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/webhook_client.py
@@ -0,0 +1,106 @@
+from typing import Any
+
+from bitbucket_cloud.client import BitbucketClient
+from loguru import logger
+from bitbucket_cloud.webhook_processors.events import (
+    RepositoryEvents,
+    PullRequestEvents,
+)
+from httpx import HTTPStatusError
+
+
+class BitbucketWebhookClient(BitbucketClient):
+    """Handles webhook operations for Bitbucket repositories."""
+
+    def __init__(self, *, secret: str | None = None, **kwargs: Any):
+        """
+        Initialize the BitbucketWebhookClient.
+
+        :param secret: Optional secret for authenticating incoming webhooks.
+        :param kwargs: Additional keyword arguments passed to the parent BitbucketClient.
+        """
+        super().__init__(**kwargs)
+        self.secret = secret
+        if self.secret:
+            logger.info(
+                "Received secret for authenticating incoming webhooks. Only authenticated webhooks will be synced."
+            )
+
+    @property
+    def _workspace_webhook_url(self) -> str:
+        """
+        Build the workspace webhook URL.
+
+        :return: The URL endpoint for workspace webhook operations.
+        """
+        return f"{self.base_url}/workspaces/{self.workspace}/hooks"
+
+    async def _webhook_exist(self, webhook_url: str) -> bool:
+        """
+        Check if a webhook with the specified URL already exists in the workspace.
+
+        :param webhook_url: The URL of the webhook to check.
+        :return: True if the webhook exists, False otherwise.
+        """
+        async for webhook_config_batch in self._send_paginated_api_request(
+            self._workspace_webhook_url
+        ):
+            if any(
+                existing_webhook_config.get("url") == webhook_url
+                for existing_webhook_config in webhook_config_batch
+            ):
+                return True
+        return False
+
+    async def create_webhook(self, app_host: str) -> None:
+        """
+        Create a new webhook for the workspace if one doesn't already exist.
+
+        Workspace webhooks are fired for events from all repositories within the workspace.
+
+        :param webhook_url: The URL to which the webhook should send events.
+        """
+        logger.info("Setting up Bitbucket webhooks for workspace: {}", self.workspace)
+
+        webhook_url = f"{app_host}/integration/webhook"
+        if await self._webhook_exist(webhook_url):
+            logger.info(
+                "Webhook already exists for workspace {} (webhook URL: {}), skipping creation.",
+                self.workspace,
+                webhook_url,
+            )
+            return
+
+        webhook_config = {
+            "description": "Port Bitbucket Integration",
+            "url": webhook_url,
+            "active": True,
+            "secret": self.secret,
+            "events": list(PullRequestEvents + RepositoryEvents),
+        }
+
+        try:
+            await self._send_api_request(
+                self._workspace_webhook_url,
+                method="POST",
+                json_data=webhook_config,
+            )
+            logger.info(
+                "Successfully created webhook for workspace {} with URL: {}",
+                self.workspace,
+                webhook_url,
+            )
+        except HTTPStatusError as http_err:
+            logger.error(
+                "HTTP error occurred while creating webhook for workspace {} with URL {}: {}",
+                self.workspace,
+                webhook_url,
+                http_err,
+            )
+        except Exception as err:
+            logger.error(
+                "Unexpected error occurred while creating webhook for workspace {} with URL {}: {}",
+                self.workspace,
+                webhook_url,
+                err,
+            )
diff --git a/integrations/bitbucket-cloud/initialize_client.py b/integrations/bitbucket-cloud/initialize_client.py
new file mode 100644
index 0000000000..283e057ab1
--- /dev/null
+++ b/integrations/bitbucket-cloud/initialize_client.py
@@ -0,0 +1,18 @@
+from port_ocean.context.ocean import ocean
+from bitbucket_cloud.client import BitbucketClient
+from bitbucket_cloud.webhook_processors.webhook_client import BitbucketWebhookClient
+
+
+def init_client() -> BitbucketClient:
+    return BitbucketClient.create_from_ocean_config()
+
+
+def init_webhook_client() -> BitbucketWebhookClient:
+    return BitbucketWebhookClient(
+        secret=ocean.integration_config["webhook_secret"],
+        workspace=ocean.integration_config["bitbucket_workspace"],
+        host=ocean.integration_config["bitbucket_host_url"],
+        username=ocean.integration_config.get("bitbucket_username"),
+        app_password=ocean.integration_config.get("bitbucket_app_password"),
+        workspace_token=ocean.integration_config.get("bitbucket_workspace_token"),
+    )
diff --git a/integrations/bitbucket-cloud/main.py b/integrations/bitbucket-cloud/main.py
index 4d2bcaa0ed..593d7b96b6 100644
--- a/integrations/bitbucket-cloud/main.py
+++ b/integrations/bitbucket-cloud/main.py
@@ -7,10 +7,17 @@
 from port_ocean.core.handlers.port_app_config.models import ResourceConfig
 from port_ocean.core.ocean_types import ASYNC_GENERATOR_RESYNC_TYPE
 from port_ocean.utils.async_iterators import stream_async_iterators_tasks
-from client import BitbucketClient
-from helpers.utils import ObjectKind
+from bitbucket_cloud.helpers.utils import ObjectKind
+
+from bitbucket_cloud.webhook_processors.processors.pull_request_webhook_processor import (
+    PullRequestWebhookProcessor,
+)
+from bitbucket_cloud.webhook_processors.processors.repository_webhook_processor import (
+    RepositoryWebhookProcessor,
+)
+from initialize_client import init_client, init_webhook_client
 from integration import BitbucketFolderResourceConfig, BitbucketFolderSelector
-from helpers.folder import (
+from bitbucket_cloud.helpers.folder import (
     process_folder_patterns,
 )
 
@@ -19,9 +26,16 @@
 async def on_start() -> None:
     logger.info("Starting Port Ocean Bitbucket integration")
 
+    if ocean.event_listener_type == "ONCE":
+        logger.info("Skipping webhook creation because the event listener is ONCE")
+        return
 
-def init_client() -> BitbucketClient:
-    return BitbucketClient.create_from_ocean_config()
+    base_url = ocean.app.base_url
+    if not base_url:
+        return
+
+    webhook_client = init_webhook_client()
+    await webhook_client.create_webhook(base_url)
 
 
 @ocean.on_resync(ObjectKind.PROJECT)
@@ -63,3 +77,7 @@ async def resync_folders(kind: str) -> ASYNC_GENERATOR_RESYNC_TYPE:
     client = init_client()
     async for matching_folders in process_folder_patterns(selector.folders, client):
         yield matching_folders
+
+
+ocean.add_webhook_processor("/webhook", PullRequestWebhookProcessor)
+ocean.add_webhook_processor("/webhook", RepositoryWebhookProcessor)
diff --git a/integrations/bitbucket-cloud/pyproject.toml b/integrations/bitbucket-cloud/pyproject.toml
index 54099c92f6..8cc4baf436 100644
--- a/integrations/bitbucket-cloud/pyproject.toml
+++ b/integrations/bitbucket-cloud/pyproject.toml
@@ -1,8 +1,8 @@
 [tool.poetry]
 name = "bitbucket-cloud"
-version = "0.1.2-dev"
+version = "0.1.3-dev"
 description = "This integration ingest data from bitbucket"
-authors = ["Adebayo Iyanuoluwa <ioluwadunsinadebayo@gmail.com>"]
+authors = ["Adebayo Iyanuoluwa <ioluwadunsinadebayo@gmail.com>", "Michael Armah <mikeyarmah@gmail.com>"]
 
 [tool.poetry.dependencies]
 python = "^3.12"
diff --git a/integrations/bitbucket-cloud/tests/conftest.py b/integrations/bitbucket-cloud/tests/conftest.py
index 42eda89572..49e9a4dc68 100644
--- a/integrations/bitbucket-cloud/tests/conftest.py
+++ b/integrations/bitbucket-cloud/tests/conftest.py
@@ -4,6 +4,7 @@
 from port_ocean.context.ocean import initialize_port_ocean_context
 from port_ocean.exceptions.context import PortOceanContextAlreadyInitializedError
 from typing import Generator
+from bitbucket_cloud.webhook_processors.webhook_client import BitbucketWebhookClient
 
 
 @pytest.fixture(autouse=True)
@@ -28,3 +29,16 @@ def mock_http_client() -> Generator[AsyncClient, None, None]:
     """Mock HTTP client for API requests."""
     with patch("client.http_async_client", new=AsyncClient()) as mock_client:
         yield mock_client
+
+
+@pytest.fixture
+def webhook_client_mock() -> BitbucketWebhookClient:
+    """Create a mocked webhook client."""
+    client = BitbucketWebhookClient(
+        workspace="test-workspace",
+        username="test-user",
+        app_password="test-password",
+        host="https://api.bitbucket.org/2.0",
+    )
+    client = MagicMock(spec=BitbucketWebhookClient)
+    return client
diff --git a/integrations/bitbucket-cloud/tests/helpers/test_folder.py b/integrations/bitbucket-cloud/tests/helpers/test_folder.py
index 12a534805b..5df6ade641 100644
--- a/integrations/bitbucket-cloud/tests/helpers/test_folder.py
+++ b/integrations/bitbucket-cloud/tests/helpers/test_folder.py
@@ -2,7 +2,7 @@
 from unittest.mock import AsyncMock
 from typing import Any, AsyncGenerator, Dict, List, Optional, Set
 
-from helpers.folder import (
+from bitbucket_cloud.helpers.folder import (
     extract_repo_names_from_patterns,
     create_pattern_mapping,
     find_matching_folders,
@@ -10,7 +10,7 @@
     process_repo_folders,
 )
 from integration import FolderPattern, RepositoryBranchMapping
-from client import BitbucketClient
+from bitbucket_cloud.client import BitbucketClient
 
 
 @pytest.fixture
diff --git a/integrations/bitbucket-cloud/tests/helpers/test_rate_limiter.py b/integrations/bitbucket-cloud/tests/helpers/test_rate_limiter.py
index 3d817c1ce1..bd1d6311df 100644
--- a/integrations/bitbucket-cloud/tests/helpers/test_rate_limiter.py
+++ b/integrations/bitbucket-cloud/tests/helpers/test_rate_limiter.py
@@ -1,7 +1,7 @@
 import pytest
 import asyncio
 import time
-from helpers.rate_limiter import RollingWindowLimiter
+from bitbucket_cloud.helpers.rate_limiter import RollingWindowLimiter
 from unittest.mock import patch
 import contextlib
 from typing import Generator
diff --git a/integrations/bitbucket-cloud/tests/test_client.py b/integrations/bitbucket-cloud/tests/test_client.py
index 21ada42732..45385e013f 100644
--- a/integrations/bitbucket-cloud/tests/test_client.py
+++ b/integrations/bitbucket-cloud/tests/test_client.py
@@ -3,8 +3,8 @@
 from httpx import AsyncClient, HTTPStatusError
 from port_ocean.context.event import event_context
 from typing import Any, AsyncIterator, Generator
-from client import BitbucketClient
-from helpers.exceptions import MissingIntegrationCredentialException
+from bitbucket_cloud.client import BitbucketClient
+from bitbucket_cloud.helpers.exceptions import MissingIntegrationCredentialException
 
 
 @pytest.fixture
@@ -35,7 +35,7 @@ async def mock_client(
     mock_integration_config: dict[str, str], mock_http_client: AsyncClient
 ) -> BitbucketClient:
     """Create BitbucketClient using create_from_ocean_config."""
-    with patch("client.http_async_client", mock_http_client):
+    with patch("bitbucket_cloud.client.http_async_client", mock_http_client):
         return BitbucketClient.create_from_ocean_config()
 
 
@@ -147,7 +147,7 @@ async def test_send_api_request_error(mock_client: BitbucketClient) -> None:
         patch.object(
             mock_client.client, "request", new_callable=AsyncMock
         ) as mock_request,
-        patch("client.logger.error") as mock_logger,
+        patch("bitbucket_cloud.client.logger.error") as mock_logger,
     ):
         mock_request.return_value = mock_response
 
diff --git a/integrations/bitbucket-cloud/tests/webhook/__init__.py b/integrations/bitbucket-cloud/tests/webhook/__init__.py
new file mode 100644
index 0000000000..c28a583aad
--- /dev/null
+++ b/integrations/bitbucket-cloud/tests/webhook/__init__.py
@@ -0,0 +1 @@
+"""Tests for Bitbucket webhook functionality."""
diff --git a/integrations/bitbucket-cloud/tests/webhook/processors/__init__.py b/integrations/bitbucket-cloud/tests/webhook/processors/__init__.py
new file mode 100644
index 0000000000..7851a968b9
--- /dev/null
+++ b/integrations/bitbucket-cloud/tests/webhook/processors/__init__.py
@@ -0,0 +1 @@
+"""Tests for Bitbucket webhook processors."""
diff --git a/integrations/bitbucket-cloud/tests/webhook/processors/test__bitbucket_abstract_webhook_processor.py b/integrations/bitbucket-cloud/tests/webhook/processors/test__bitbucket_abstract_webhook_processor.py
new file mode 100644
index 0000000000..b07e37f88d
--- /dev/null
+++ b/integrations/bitbucket-cloud/tests/webhook/processors/test__bitbucket_abstract_webhook_processor.py
@@ -0,0 +1,202 @@
+import pytest
+import json
+import hashlib
+import hmac
+from unittest.mock import MagicMock, patch
+from typing import Any
+from fastapi import Request
+from port_ocean.core.handlers.webhook.webhook_event import (
+    WebhookEvent,
+)
+from bitbucket_cloud.webhook_processors.webhook_client import BitbucketWebhookClient
+
+# Patch the module before importing the class
+with patch("initialize_client.init_webhook_client") as mock_init_client:
+    from bitbucket_cloud.webhook_processors.processors._bitbucket_abstract_webhook_processor import (
+        _BitbucketAbstractWebhookProcessor,
+    )
+
+
+# Create a concrete implementation for testing the abstract class
+class ConcreteWebhookProcessor(_BitbucketAbstractWebhookProcessor):
+    """Concrete implementation of the abstract webhook processor for testing."""
+
+    async def _should_process_event(self, event: WebhookEvent) -> bool:
+        """Test implementation of the abstract method."""
+        # Process events with a specific test header
+        return event.headers.get("x-test-event") == "process"
+
+    async def get_matching_kinds(self, event: WebhookEvent) -> list[str]:
+        """Test implementation of the abstract method."""
+        return ["test_kind"]
+
+    async def handle_event(self, payload: dict[str, Any], resource_config: Any) -> Any:
+        """Test implementation of the abstract method."""
+        return {"test": "result"}
+
+    async def validate_payload(self, payload: dict[str, Any]) -> bool:
+        """Test implementation of the abstract method."""
+        return True
+
+    # Add a property to fix the issue with self.secret in _verify_webhook_signature
+    @property
+    def secret(self) -> str:
+        """Return the secret from the webhook client."""
+        return self._webhook_client.secret if self._webhook_client.secret else ""
+
+
+# Create a mock request for testing
+def create_mock_request(body: bytes, headers: dict[str, str]) -> Request:
+    """Create a mock FastAPI Request object for testing."""
+    mock_request = MagicMock(spec=Request)
+    mock_request.headers = headers
+
+    # Create a mock body() coroutine that returns the bytes
+    async def mock_body() -> bytes:
+        return body
+
+    mock_request.body = mock_body
+    return mock_request
+
+
+def compute_signature(secret: str, payload: dict[str, Any]) -> str:
+    """Compute the HMAC-SHA256 signature in the expected format."""
+    payload_bytes = json.dumps(payload).encode("utf-8")
+    return (
+        "sha256="
+        + hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
+    )
+
+
+@pytest.fixture
+def event() -> WebhookEvent:
+    """Fixture to create a basic webhook event."""
+    return WebhookEvent(trace_id="test-trace-id", payload={}, headers={})
+
+
+@pytest.fixture
+def webhook_processor(
+    event: WebhookEvent, webhook_client_mock: BitbucketWebhookClient
+) -> ConcreteWebhookProcessor:
+    """Create a concrete webhook processor with a mocked client."""
+    processor = ConcreteWebhookProcessor(event)
+    processor._webhook_client = webhook_client_mock
+    return processor
+
+
+class TestBitbucketAbstractWebhookProcessor:
+    """Tests for the _BitbucketAbstractWebhookProcessor class."""
+
+    @pytest.mark.asyncio
+    async def test_authenticate(
+        self, webhook_processor: ConcreteWebhookProcessor
+    ) -> None:
+        """Test the authenticate method."""
+        result = await webhook_processor.authenticate({}, {})
+        assert result is True
+
+    @pytest.mark.asyncio
+    async def test_verify_webhook_signature_no_secret(
+        self, webhook_processor: ConcreteWebhookProcessor
+    ) -> None:
+        """Test signature verification when no secret is configured."""
+        # Mock the webhook client with no secret
+        webhook_processor._webhook_client.secret = None
+
+        mock_request = MagicMock(spec=Request)
+
+        result = await webhook_processor._verify_webhook_signature(mock_request)
+        assert result is True
+
+    @pytest.mark.asyncio
+    async def test_verify_webhook_signature_no_header(
+        self, webhook_processor: ConcreteWebhookProcessor
+    ) -> None:
+        """Test signature verification when no signature header is provided."""
+        # Mock the webhook client with a secret
+        webhook_processor._webhook_client.secret = "test-secret"
+
+        mock_request = create_mock_request(b"{}", {})
+
+        result = await webhook_processor._verify_webhook_signature(mock_request)
+        assert result is False
+
+    @pytest.mark.asyncio
+    async def test_verify_webhook_signature_valid(
+        self, webhook_processor: ConcreteWebhookProcessor
+    ) -> None:
+        """Test signature verification with a valid signature."""
+        # Set up the test
+        secret = "test-secret"
+        webhook_processor._webhook_client.secret = secret
+
+        payload = {"test": "data"}
+        payload_bytes = json.dumps(payload).encode("utf-8")
+
+        valid_signature = compute_signature(secret, payload)
+        headers = {"x-hub-signature": valid_signature}
+
+        mock_request = create_mock_request(payload_bytes, headers)
+
+        result = await webhook_processor._verify_webhook_signature(mock_request)
+        assert result is True
+
+    @pytest.mark.asyncio
+    async def test_verify_webhook_signature_invalid(
+        self, webhook_processor: ConcreteWebhookProcessor
+    ) -> None:
+        """Test signature verification with an invalid signature."""
+        # Set up the test
+        secret = "test-secret"
+        webhook_processor._webhook_client.secret = secret
+
+        payload = {"test": "data"}
+        payload_bytes = json.dumps(payload).encode("utf-8")
+
+        invalid_signature = "sha256=invalid"
+        headers = {"x-hub-signature": invalid_signature}
+
+        mock_request = create_mock_request(payload_bytes, headers)
+
+        result = await webhook_processor._verify_webhook_signature(mock_request)
+        assert result is False
+
+    @pytest.mark.asyncio
+    async def test_should_process_event_no_request(
+        self, webhook_processor: ConcreteWebhookProcessor
+    ) -> None:
+        """Test should_process_event when no original request is available."""
+        event = WebhookEvent(
+            trace_id="test-trace-id",
+            headers={"x-test-event": "process"},
+            payload={},
+        )
+        # Ensure there's no original request
+        event._original_request = None
+
+        result = await webhook_processor.should_process_event(event)
+        assert result is False
+
+    @pytest.mark.asyncio
+    async def test_should_process_event_implementation_returns_false(
+        self, webhook_processor: ConcreteWebhookProcessor
+    ) -> None:
+        """Test should_process_event when the implementation returns False."""
+        payload = {"test": "data"}
+        payload_bytes = json.dumps(payload).encode("utf-8")
+        headers = {"x-test-event": "do-not-process"}
+
+        mock_request = create_mock_request(payload_bytes, headers)
+
+        event = WebhookEvent(
+            trace_id="test-trace-id",
+            headers=headers,
+            payload=payload,
+        )
+        event._original_request = mock_request
+
+        # Update the webhook processor with this event
+        webhook_processor.event = event
+
+        result = await webhook_processor.should_process_event(event)
+        assert result is False
diff --git a/integrations/bitbucket-cloud/tests/webhook/processors/test_pull_request_webhook_processor.py b/integrations/bitbucket-cloud/tests/webhook/processors/test_pull_request_webhook_processor.py
new file mode 100644
index 0000000000..b212bcf08c
--- /dev/null
+++ b/integrations/bitbucket-cloud/tests/webhook/processors/test_pull_request_webhook_processor.py
@@ -0,0 +1,130 @@
+import pytest
+from unittest.mock import MagicMock, patch
+from typing import Any
+from port_ocean.core.handlers.webhook.webhook_event import (
+    WebhookEvent,
+    WebhookEventRawResults,
+)
+from bitbucket_cloud.helpers.utils import ObjectKind
+
+# Patch the module before importing the class
+with patch("initialize_client.init_webhook_client") as mock_init_client:
+    from bitbucket_cloud.webhook_processors.webhook_client import BitbucketWebhookClient
+    from bitbucket_cloud.webhook_processors.processors.pull_request_webhook_processor import (
+        PullRequestWebhookProcessor,
+    )
+
+
+@pytest.fixture
+def event() -> WebhookEvent:
+    return WebhookEvent(trace_id="test-trace-id", payload={}, headers={})
+
+
+@pytest.fixture
+def pull_request_webhook_processor(
+    webhook_client_mock: BitbucketWebhookClient, event: WebhookEvent
+) -> PullRequestWebhookProcessor:
+    """Create a PullRequestWebhookProcessor with mocked webhook client."""
+    pull_request_webhook_processor = PullRequestWebhookProcessor(event)
+    pull_request_webhook_processor._webhook_client = webhook_client_mock
+    return pull_request_webhook_processor
+
+
+class TestPullRequestWebhookProcessor:
+
+    @pytest.mark.parametrize(
+        "event_key, expected",
+        [
+            ("pullrequest:created", True),
+            ("pullrequest:updated", True),
+            ("pullrequest:approved", True),
+            ("pullrequest:unapproved", True),
+            ("pullrequest:fulfilled", True),
+            ("pullrequest:rejected", True),
+            ("repo:created", False),
+            ("repo:updated", False),
+            ("repo:deleted", False),
+            ("invalid:event", False),
+        ],
+    )
+    @pytest.mark.asyncio
+    async def test_should_process_event(
+        self,
+        pull_request_webhook_processor: PullRequestWebhookProcessor,
+        event_key: str,
+        expected: bool,
+    ) -> None:
+        """Test that should_process_event correctly identifies valid PR events."""
+        event = WebhookEvent(
+            trace_id="test-trace-id", headers={"x-event-key": event_key}, payload={}
+        )
+        result = await pull_request_webhook_processor._should_process_event(event)
+        assert result == expected
+
+    @pytest.mark.asyncio
+    async def test_get_matching_kinds(
+        self, pull_request_webhook_processor: PullRequestWebhookProcessor
+    ) -> None:
+        """Test that get_matching_kinds returns the PULL_REQUEST kind."""
+        event = WebhookEvent(
+            trace_id="test-trace-id",
+            headers={"x-event-key": "pullrequest:created"},
+            payload={},
+        )
+        result = await pull_request_webhook_processor.get_matching_kinds(event)
+        assert result == [ObjectKind.PULL_REQUEST]
+
+    @pytest.mark.asyncio
+    async def test_handle_event(
+        self,
+        pull_request_webhook_processor: PullRequestWebhookProcessor,
+        webhook_client_mock: MagicMock,
+    ) -> None:
+        """Test handling a pull request event."""
+        # Arrange
+        payload = {"repository": {"uuid": "repo-123"}, "pullrequest": {"id": "pr-456"}}
+        resource_config = MagicMock()
+
+        webhook_client_mock.get_pull_request.return_value = {
+            "id": "pr-456",
+            "title": "Test PR",
+            "description": "This is a test pull request",
+        }
+
+        # Act
+        result = await pull_request_webhook_processor.handle_event(
+            payload, resource_config
+        )
+
+        # Assert
+        webhook_client_mock.get_pull_request.assert_called_once_with(
+            "repo-123", "pr-456"
+        )
+        assert isinstance(result, WebhookEventRawResults)
+        assert len(result.updated_raw_results) == 1
+        assert result.updated_raw_results[0] == {
+            "id": "pr-456",
+            "title": "Test PR",
+            "description": "This is a test pull request",
+        }
+        assert len(result.deleted_raw_results) == 0
+
+    @pytest.mark.parametrize(
+        "payload, expected",
+        [
+            ({"repository": {}, "pullrequest": {}}, True),
+            ({"repository": {}}, False),
+            ({"pullrequest": {}}, False),
+            ({}, False),
+        ],
+    )
+    @pytest.mark.asyncio
+    async def test_validate_payload(
+        self,
+        pull_request_webhook_processor: PullRequestWebhookProcessor,
+        payload: dict[str, Any],
+        expected: bool,
+    ) -> None:
+        """Test payload validation with various input scenarios."""
+        result = await pull_request_webhook_processor.validate_payload(payload)
+        assert result == expected
diff --git a/integrations/bitbucket-cloud/tests/webhook/processors/test_repository_webhook_processor.py b/integrations/bitbucket-cloud/tests/webhook/processors/test_repository_webhook_processor.py
new file mode 100644
index 0000000000..2ed3f01017
--- /dev/null
+++ b/integrations/bitbucket-cloud/tests/webhook/processors/test_repository_webhook_processor.py
@@ -0,0 +1,123 @@
+import pytest
+from unittest.mock import MagicMock, patch
+from typing import Any
+from port_ocean.core.handlers.webhook.webhook_event import (
+    WebhookEvent,
+    WebhookEventRawResults,
+)
+from bitbucket_cloud.helpers.utils import ObjectKind
+from bitbucket_cloud.webhook_processors.webhook_client import BitbucketWebhookClient
+
+# Patch the module before importing the class
+with patch("initialize_client.init_webhook_client") as mock_init_client:
+    from bitbucket_cloud.webhook_processors.processors.repository_webhook_processor import (
+        RepositoryWebhookProcessor,
+    )
+
+
+@pytest.fixture
+def event() -> WebhookEvent:
+    return WebhookEvent(trace_id="test-trace-id", payload={}, headers={})
+
+
+@pytest.fixture
+def repository_webhook_processor(
+    webhook_client_mock: BitbucketWebhookClient, event: WebhookEvent
+) -> RepositoryWebhookProcessor:
+    """Create a RepositoryWebhookProcessor with mocked webhook client."""
+    repository_webhook_processor = RepositoryWebhookProcessor(event)
+    repository_webhook_processor._webhook_client = webhook_client_mock
+    return repository_webhook_processor
+
+
+class TestRepositoryWebhookProcessor:
+
+    @pytest.mark.parametrize(
+        "event_key, expected",
+        [
+            ("repo:created", True),
+            ("repo:updated", True),
+            ("repo:push", True),
+            ("pullrequest:created", False),
+            ("pullrequest:updated", False),
+            ("invalid:event", False),
+        ],
+    )
+    @pytest.mark.asyncio
+    async def test_should_process_event(
+        self,
+        repository_webhook_processor: RepositoryWebhookProcessor,
+        event_key: str,
+        expected: bool,
+    ) -> None:
+        """Test that should_process_event correctly identifies valid repository events."""
+        event = WebhookEvent(
+            trace_id="test-trace-id", headers={"x-event-key": event_key}, payload={}
+        )
+        result = await repository_webhook_processor._should_process_event(event)
+        assert result == expected
+
+    @pytest.mark.asyncio
+    async def test_get_matching_kinds(
+        self, repository_webhook_processor: RepositoryWebhookProcessor
+    ) -> None:
+        """Test that get_matching_kinds returns the REPOSITORY kind."""
+        event = WebhookEvent(
+            trace_id="test-trace-id",
+            headers={"x-event-key": "repo:created"},
+            payload={},
+        )
+        result = await repository_webhook_processor.get_matching_kinds(event)
+        assert result == [ObjectKind.REPOSITORY]
+
+    @pytest.mark.asyncio
+    async def test_handle_event(
+        self,
+        repository_webhook_processor: RepositoryWebhookProcessor,
+        webhook_client_mock: MagicMock,
+    ) -> None:
+        """Test handling a repository event."""
+        # Arrange
+        payload = {"repository": {"uuid": "repo-123"}}
+        resource_config = MagicMock()
+
+        webhook_client_mock.get_repository.return_value = {
+            "uuid": "repo-123",
+            "name": "Test Repository",
+            "description": "This is a test repository",
+        }
+
+        # Act
+        result = await repository_webhook_processor.handle_event(
+            payload, resource_config
+        )
+
+        # Assert
+        webhook_client_mock.get_repository.assert_called_once_with("repo-123")
+        assert isinstance(result, WebhookEventRawResults)
+        assert len(result.updated_raw_results) == 1
+        assert result.updated_raw_results[0] == {
+            "uuid": "repo-123",
+            "name": "Test Repository",
+            "description": "This is a test repository",
+        }
+        assert len(result.deleted_raw_results) == 0
+
+    @pytest.mark.parametrize(
+        "payload, expected",
+        [
+            ({"repository": {}}, True),
+            ({}, False),
+            ({"pullrequest": {}}, False),
+        ],
+    )
+    @pytest.mark.asyncio
+    async def test_validate_payload(
+        self,
+        repository_webhook_processor: RepositoryWebhookProcessor,
+        payload: dict[str, Any],
+        expected: bool,
+    ) -> None:
+        """Test payload validation with various input scenarios."""
+        result = await repository_webhook_processor.validate_payload(payload)
+        assert result == expected
diff --git a/integrations/bitbucket-cloud/tests/webhook/test_events.py b/integrations/bitbucket-cloud/tests/webhook/test_events.py
new file mode 100644
index 0000000000..816114b6d3
--- /dev/null
+++ b/integrations/bitbucket-cloud/tests/webhook/test_events.py
@@ -0,0 +1,76 @@
+import pytest
+from bitbucket_cloud.webhook_processors.events import (
+    PullRequestEvents,
+    RepositoryEvents,
+)
+
+
+class TestPullRequestEvents:
+    def test_values(self) -> None:
+        """Test that PullRequestEvents.values() returns all expected events."""
+        expected_values = {
+            "pullrequest:created",
+            "pullrequest:updated",
+            "pullrequest:approved",
+            "pullrequest:unapproved",
+            "pullrequest:fulfilled",
+            "pullrequest:rejected",
+        }
+        assert PullRequestEvents.values() == expected_values
+
+    def test_enum_access(self) -> None:
+        """Test that enum values can be accessed as attributes and by value."""
+        assert PullRequestEvents.PULL_REQUEST_CREATED == "pullrequest:created"
+        assert PullRequestEvents.PULL_REQUEST_UPDATED == "pullrequest:updated"
+        assert (
+            PullRequestEvents("pullrequest:created")
+            == PullRequestEvents.PULL_REQUEST_CREATED
+        )
+
+
+class TestRepositoryEvents:
+    def test_values(self) -> None:
+        """Test that RepositoryEvents.values() returns all expected events."""
+        expected_values = {"repo:created", "repo:updated", "repo:push"}
+        assert RepositoryEvents.values() == expected_values
+
+    def test_enum_access(self) -> None:
+        """Test that enum values can be accessed as attributes and by value."""
+        assert RepositoryEvents.REPOSITORY_CREATED == "repo:created"
+        assert RepositoryEvents.REPOSITORY_UPDATED == "repo:updated"
+        assert RepositoryEvents("repo:created") == RepositoryEvents.REPOSITORY_CREATED
+
+
+class TestEventEnumMeta:
+    def test_add_two_enums(self) -> None:
+        """Test that adding two EventEnums returns a union of their values."""
+        combined = PullRequestEvents + RepositoryEvents
+        expected = PullRequestEvents.values() | RepositoryEvents.values()
+        assert combined == expected
+
+    def test_add_enum_with_set(self) -> None:
+        """Test that adding an EventEnum with a set returns a union of values."""
+        custom_events = {"custom:event1", "custom:event2"}
+        combined = PullRequestEvents + custom_events
+        expected = PullRequestEvents.values() | custom_events
+        assert combined == expected
+
+    def test_radd_enum_with_set(self) -> None:
+        """Test that right-adding an EventEnum with a set returns a union of values."""
+        custom_events = {"custom:event1", "custom:event2"}
+        combined = custom_events + PullRequestEvents
+        expected = custom_events | PullRequestEvents.values()
+        assert combined == expected
+
+    def test_add_with_incompatible(self) -> None:
+        """Test adding with an incompatible type raises TypeError."""
+        with pytest.raises(TypeError):
+            PullRequestEvents + 42
+
+        with pytest.raises(TypeError):
+            PullRequestEvents + None
+
+    def test_radd_with_incompatible(self) -> None:
+        """Test that __radd__ with an incompatible type returns NotImplemented."""
+        result = PullRequestEvents.__radd__(42)
+        assert result is NotImplemented
diff --git a/integrations/bitbucket-cloud/tests/webhook/test_webhook_client.py b/integrations/bitbucket-cloud/tests/webhook/test_webhook_client.py
new file mode 100644
index 0000000000..b3685f1cd3
--- /dev/null
+++ b/integrations/bitbucket-cloud/tests/webhook/test_webhook_client.py
@@ -0,0 +1,129 @@
+import pytest
+from unittest.mock import AsyncMock, patch
+from typing import Any, AsyncGenerator
+from bitbucket_cloud.webhook_processors.webhook_client import BitbucketWebhookClient
+
+
+@pytest.fixture
+def webhook_client_with_secret() -> BitbucketWebhookClient:
+    """Create a BitbucketWebhookClient with a secret."""
+    return BitbucketWebhookClient(
+        host="https://api.bitbucket.org/2.0",
+        secret="test-secret",
+        workspace="test-workspace",
+        username="test-user",
+        app_password="test-password",
+    )
+
+
+@pytest.fixture
+def webhook_client_no_secret() -> BitbucketWebhookClient:
+    """Create a BitbucketWebhookClient without a secret."""
+    return BitbucketWebhookClient(
+        host="https://api.bitbucket.org/2.0",
+        workspace="test-workspace",
+        username="test-user",
+        app_password="test-password",
+    )
+
+
+class TestBitbucketWebhookClient:
+
+    def test_init_with_secret(
+        self, webhook_client_with_secret: BitbucketWebhookClient
+    ) -> None:
+        """Test initialization with a secret."""
+        assert webhook_client_with_secret.secret == "test-secret"
+
+    def test_init_without_secret(
+        self, webhook_client_no_secret: BitbucketWebhookClient
+    ) -> None:
+        """Test initialization without a secret."""
+        assert webhook_client_no_secret.secret is None
+
+    def test_workspace_webhook_url(
+        self, webhook_client_with_secret: BitbucketWebhookClient
+    ) -> None:
+        """Test that the workspace webhook URL is correctly formed."""
+        expected = "https://api.bitbucket.org/2.0/workspaces/test-workspace/hooks"
+        assert webhook_client_with_secret._workspace_webhook_url == expected
+
+    @pytest.mark.asyncio
+    async def test_webhook_exist_not_found(
+        self, webhook_client_with_secret: BitbucketWebhookClient
+    ) -> None:
+        """Test that _webhook_exist returns False when no matching webhook is found."""
+
+        async def fake_paginated_response(
+            api_url: str,
+        ) -> AsyncGenerator[list[dict[str, Any]], None]:
+            yield []  # Simulate an empty batch
+
+        with patch.object(
+            webhook_client_with_secret,
+            "_send_paginated_api_request",
+            new=fake_paginated_response,
+        ):
+            result = await webhook_client_with_secret._webhook_exist(
+                "https://example.com/webhook"
+            )
+            assert result is False
+
+    @pytest.mark.asyncio
+    async def test_webhook_exist_found(
+        self, webhook_client_with_secret: BitbucketWebhookClient
+    ) -> None:
+        """Test that _webhook_exist returns True when a matching webhook is found."""
+
+        async def fake_paginated_response(
+            api_url: str,
+        ) -> AsyncGenerator[list[dict[str, Any]], None]:
+            yield [{"url": "https://example.com/webhook", "id": "hook-123"}]
+
+        with patch.object(
+            webhook_client_with_secret,
+            "_send_paginated_api_request",
+            new=fake_paginated_response,
+        ):
+            result = await webhook_client_with_secret._webhook_exist(
+                "https://example.com/webhook"
+            )
+            assert result is True
+
+    @pytest.mark.asyncio
+    async def test_create_webhook_when_not_exist(
+        self, webhook_client_with_secret: BitbucketWebhookClient
+    ) -> None:
+        """Test that create_webhook creates a new webhook when none exists."""
+        # Simulate that no webhook exists.
+        with patch.object(
+            webhook_client_with_secret,
+            "_webhook_exist",
+            new=AsyncMock(return_value=False),
+        ):
+            # Patch _send_api_request to simulate a successful webhook creation.
+            with patch.object(
+                webhook_client_with_secret,
+                "_send_api_request",
+                new=AsyncMock(return_value={"id": "hook-123"}),
+            ) as mock_send:
+                await webhook_client_with_secret.create_webhook("https://example.com")
+                mock_send.assert_called_once()
+
+    @pytest.mark.asyncio
+    async def test_create_webhook_when_already_exists(
+        self, webhook_client_with_secret: BitbucketWebhookClient
+    ) -> None:
+        """Test that create_webhook does not create a webhook if one already exists."""
+        # Simulate that the webhook already exists.
+        with patch.object(
+            webhook_client_with_secret,
+            "_webhook_exist",
+            new=AsyncMock(return_value=True),
+        ):
+            # Patch _send_api_request and verify it is not called.
+            with patch.object(
+                webhook_client_with_secret, "_send_api_request", new=AsyncMock()
+            ) as mock_send:
+                await webhook_client_with_secret.create_webhook("https://example.com")
+                mock_send.assert_not_called()


Reviews:
user: login: yaelibarg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-23T09:50:36Z
user: login: yaelibarg
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T14:45:50Z
user: login: yaelibarg
body: <img src="https://media0.giphy.com/media/v1.Y2lkPWJkM2VhNTdlbnowcjR3ZDd6a2JnMjRpNTR3ZWZzeGsxd3cxeXRiZHF1bDFtdmg2ZiZlcD12MV9naWZzX3RyZW5kaW5nJmN0PWc/6XpuqzNVg5wzupU2yR/giphy-downsized-medium.gif"/>
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T17:22:22Z

Review comments:
diff_hunk: @@ -30,3 +30,7 @@ configurations:
     type: string
     description: The base api url of Bitbucket Cloud Rest API. It should include the protocol and version (e.g. <a href="https://api.bitbucket.org/2.0" target="_blank">https://api.bitbucket.org/2.0</a>).
     default: https://api.bitbucket.org/2.0
+  - name: bitbucketWebhookSecret
path: integrations/bitbucket-cloud/.port/spec.yaml
user: login: yaelibarg
body: name: webhookSecret
sensitive: true
created_at: 2025-03-23T09:32:16Z
author_association: CONTRIBUTOR
path: integrations/bitbucket-cloud/bitbucket_cloud/webhook/processors/pull_request.py
user: login: yaelibarg
body: lets stay in the consistent with our naming convention 
folder name should be webhook_processors
pull_request_webhook_processor.py
created_at: 2025-03-23T09:37:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,141 @@
+from typing import Any
+
+from bitbucket_cloud.client import BitbucketClient
+from loguru import logger
+from bitbucket_cloud.webhook_processors.events import (
+    RepositoryEvents,
+    PullRequestEvents,
+)
+from httpx import HTTPStatusError
+import hashlib
+import hmac
+
+from fastapi import Request
+
+
+class BitbucketWebhookClient(BitbucketClient):
+    """Handles webhook operations for Bitbucket repositories."""
+
+    def __init__(self, *, secret: str | None = None, **kwargs: Any):
+        """
+        Initialize the BitbucketWebhookClient.
+
+        :param secret: Optional secret for authenticating incoming webhooks.
+        :param kwargs: Additional keyword arguments passed to the parent BitbucketClient.
+        """
+        super().__init__(**kwargs)
+        self.secret = secret
+        if self.secret:
+            logger.info(
+                "Received secret for authenticating incoming webhooks. Only authenticated webhooks will be synced."
+            )
+
+    async def authenticate_incoming_webhook(self, request: Request) -> bool:
path: integrations/bitbucket-cloud/bitbucket_cloud/webhook_processors/webhook_client.py
user: login: yaelibarg
body:  would do this entire logic in the webhook processor, its not really the client's responsibility 
created_at: 2025-03-24T14:44:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: aws-amplify-eu-west-1[bot]
created_at: 2025-03-18T16:20:11Z
body: This pull request is automatically being deployed by Amplify Hosting ([learn more](https://aws.amazon.com/amplify/hosting/)).<br/><br/>Access this pull request here: https://pr-1500.d1ftd8v2gowp8w.amplifyapp.com
author_association: NONE
user: login: qodo-code-review[bot]
created_at: 2025-03-24T19:37:18Z
body: ## CI Feedback 🧐

#### (Feedback updated until commit https://github.com/port-labs/ocean/commit/066413ae8233d98e78b6f6d855e12bd0a23d08fa)


A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

<table><tr><td>

**Action:** 🚢 bitbucket-cloud</td></tr>
<tr><td>

**Failed stage:** [Test](https://github.com/port-labs/ocean/actions/runs/14044343613/job/39321589999) [❌]

</td></tr>
<tr><td>

**Failed test name:** TestRepositoryWebhookProcessor.test_should_process_event[repo:deleted-True], TestRepositoryEvents.test_values


</td></tr>
<tr><td>

**Failure summary:**

The action failed because two tests failed:<br> 1. <br><code>TestRepositoryWebhookProcessor.test_should_process_event[repo:deleted-True]</code> - The test expected <code>True</code> <br>but got <code>False</code> when checking if the repository webhook processor should process a 'repo:deleted' <br>event.<br> 2. <code>TestRepositoryEvents.test_values</code> - The test expected the <code>RepositoryEvents.values()</code> to <br>include 'repo:deleted' but instead it contained 'repo:push'. The actual set was {'repo:created', <br>'repo:push', 'repo:updated'} while the expected set was {'repo:created', 'repo:deleted', <br>'repo:updated'}.<br> <br> Both failures appear to be related to the same issue: the 'repo:deleted' event is expected to be <br>supported but is not implemented correctly in the code.<br>

</td></tr>
<tr><td>

<details><summary>Relevant error logs:</summary>


```yaml
1:  ##[group]Operating System
2:  Ubuntu
...

981:  [gw0] [  3%] PASSED tests/helpers/test_folder.py::test_extract_repo_names_from_patterns_empty 
982:  tests/helpers/test_folder.py::test_extract_repo_names_from_patterns_no_repos 
983:  [gw0] [  5%] PASSED tests/helpers/test_folder.py::test_extract_repo_names_from_patterns_no_repos 
984:  [gw1] [  6%] PASSED tests/helpers/test_folder.py::test_process_folder_patterns 
985:  tests/helpers/test_folder.py::test_process_repo_folders 
986:  [gw1] [  7%] PASSED tests/helpers/test_folder.py::test_process_repo_folders 
987:  tests/helpers/test_folder.py::test_create_pattern_mapping 
988:  tests/helpers/test_rate_limiter.py::test_basic_rate_limiting 
989:  [gw0] [  8%] PASSED tests/helpers/test_folder.py::test_create_pattern_mapping 
990:  tests/helpers/test_rate_limiter.py::test_purge_behavior 
991:  [gw1] [ 10%] PASSED tests/helpers/test_rate_limiter.py::test_basic_rate_limiting 
992:  tests/test_client.py::test_send_api_request_success 
993:  [gw1] [ 11%] PASSED tests/test_client.py::test_send_api_request_success 
994:  tests/test_client.py::test_send_api_request_with_params 
995:  [gw1] [ 12%] PASSED tests/test_client.py::test_send_api_request_with_params 
996:  tests/test_client.py::test_send_api_request_error 
997:  [gw1] [ 13%] PASSED tests/test_client.py::test_send_api_request_error 
998:  tests/test_client.py::test_send_paginated_api_request 
...

1048:  tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_handle_event 
1049:  [gw1] [ 46%] PASSED tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_handle_event 
1050:  tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_validate_payload[payload0-True] 
1051:  [gw1] [ 48%] PASSED tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_validate_payload[payload0-True] 
1052:  tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_validate_payload[payload1-False] 
1053:  [gw1] [ 49%] PASSED tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_validate_payload[payload1-False] 
1054:  tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_validate_payload[payload2-False] 
1055:  [gw1] [ 50%] PASSED tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_validate_payload[payload2-False] 
1056:  tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_validate_payload[payload3-False] 
1057:  [gw1] [ 51%] PASSED tests/webhook/processors/test_pull_request_webhook_processor.py::TestPullRequestWebhookProcessor::test_validate_payload[payload3-False] 
1058:  tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_should_process_event[repo:created-True] 
1059:  [gw1] [ 53%] PASSED tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_should_process_event[repo:created-True] 
1060:  tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_should_process_event[repo:updated-True] 
1061:  [gw1] [ 54%] PASSED tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_should_process_event[repo:updated-True] 
1062:  tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_should_process_event[repo:deleted-True] 
1063:  [gw1] [ 55%] FAILED tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_should_process_event[repo:deleted-True] 
1064:  [gw2] [ 56%] PASSED tests/helpers/test_rate_limiter.py::test_rolling_window_behavior 
...

1074:  tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_get_matching_kinds 
1075:  [gw3] [ 63%] PASSED tests/helpers/test_rate_limiter.py::test_edge_cases 
1076:  tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_handle_event 
1077:  [gw1] [ 64%] PASSED tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_get_matching_kinds 
1078:  tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_validate_payload[payload0-True] 
1079:  [gw0] [ 65%] PASSED tests/helpers/test_rate_limiter.py::test_purge_behavior 
1080:  [gw1] [ 67%] PASSED tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_validate_payload[payload0-True] 
1081:  tests/helpers/test_rate_limiter.py::test_wait_for_next_slot 
1082:  tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_validate_payload[payload2-False] 
1083:  [gw1] [ 68%] PASSED tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_validate_payload[payload2-False] 
1084:  tests/webhook/test_events.py::TestPullRequestEvents::test_values 
1085:  [gw1] [ 69%] PASSED tests/webhook/test_events.py::TestPullRequestEvents::test_values 
1086:  tests/webhook/test_events.py::TestPullRequestEvents::test_enum_access 
1087:  [gw1] [ 70%] PASSED tests/webhook/test_events.py::TestPullRequestEvents::test_enum_access 
1088:  tests/webhook/test_events.py::TestRepositoryEvents::test_values 
1089:  [gw1] [ 72%] FAILED tests/webhook/test_events.py::TestRepositoryEvents::test_values 
1090:  tests/webhook/test_events.py::TestRepositoryEvents::test_enum_access 
...

1117:  tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_validate_payload[payload1-False] 
1118:  [gw3] [ 91%] PASSED tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_validate_payload[payload1-False] 
1119:  [gw2] [ 92%] PASSED tests/helpers/test_rate_limiter.py::test_concurrent_requests 
1120:  tests/helpers/test_rate_limiter.py::test_context_manager 
1121:  [gw0] [ 93%] PASSED tests/helpers/test_rate_limiter.py::test_wait_for_next_slot 
1122:  tests/test_client.py::test_client_init_with_token 
1123:  [gw0] [ 94%] PASSED tests/test_client.py::test_client_init_with_token 
1124:  tests/test_client.py::test_client_init_with_app_password 
1125:  [gw0] [ 96%] PASSED tests/test_client.py::test_client_init_with_app_password 
1126:  tests/test_client.py::test_client_init_no_auth 
1127:  [gw0] [ 97%] PASSED tests/test_client.py::test_client_init_no_auth 
1128:  [gw2] [ 98%] PASSED tests/helpers/test_rate_limiter.py::test_context_manager 
1129:  tests/helpers/test_rate_limiter.py::test_metrics 
1130:  2025-03-24 19:38:02.075 | INFO     | bitbucket_cloud.helpers.rate_limiter:__init__:125 - Created rate limiter: limit=980, window=3600s, purge_interval=1.0s, request_timeout=Nones
1131:  [gw2] [100%] PASSED tests/helpers/test_rate_limiter.py::test_metrics 
1132:  =================================== FAILURES ===================================
1133:  _ TestRepositoryWebhookProcessor.test_should_process_event[repo:deleted-True] __
...

1148:  )
1149:  @pytest.mark.asyncio
1150:  async def test_should_process_event(
1151:  self,
1152:  repository_webhook_processor: RepositoryWebhookProcessor,
1153:  event_key: str,
1154:  expected: bool,
1155:  ) -> None:
1156:  """Test that should_process_event correctly identifies valid repository events."""
1157:  event = WebhookEvent(
1158:  trace_id="test-trace-id", headers={"x-event-key": event_key}, payload={}
1159:  )
1160:  result = await repository_webhook_processor._should_process_event(event)
1161:  >       assert result == expected
1162:  E       assert False == True
1163:  tests/webhook/processors/test_repository_webhook_processor.py:58: AssertionError
1164:  _______________________ TestRepositoryEvents.test_values _______________________
1165:  [gw1] linux -- Python 3.12.9 /home/runner/work/ocean/ocean/integrations/bitbucket-cloud/.venv/bin/python
1166:  self = <tests.webhook.test_events.TestRepositoryEvents object at 0x7f30a4db8620>
1167:  def test_values(self) -> None:
1168:  """Test that RepositoryEvents.values() returns all expected events."""
1169:  expected_values = {"repo:created", "repo:updated", "repo:deleted"}
1170:  >       assert RepositoryEvents.values() == expected_values
1171:  E       AssertionError: assert {'repo:created', 'repo:push', 'repo:updated'} == {'repo:created', 'repo:deleted', 'repo:updated'}
1172:  E         
1173:  E         Extra items in the left set:
1174:  E         'repo:push'
1175:  E         Extra items in the right set:
1176:  E         'repo:deleted'
1177:  E         
1178:  E         Full diff:
1179:  E           {
1180:  E               'repo:created',
1181:  E         -     'repo:deleted',
1182:  E         +     'repo:push',
1183:  E               'repo:updated',
1184:  E           }
1185:  tests/webhook/test_events.py:35: AssertionError
1186:  - generated xml file: /home/runner/work/ocean/ocean/integrations/bitbucket-cloud/junit/test-results-integrations/bitbucket-cloud.xml -
1187:  =========================== short test summary info ============================
1188:  FAILED tests/webhook/processors/test_repository_webhook_processor.py::TestRepositoryWebhookProcessor::test_should_process_event[repo:deleted-True] - assert False == True
1189:  FAILED tests/webhook/test_events.py::TestRepositoryEvents::test_values - AssertionError: assert {'repo:created', 'repo:push', 'repo:updated'} == {'repo:created', 'repo:deleted', 'repo:updated'}
1190:  Extra items in the left set:
1191:  'repo:push'
1192:  Extra items in the right set:
1193:  'repo:deleted'
1194:  Full diff:
1195:  {
1196:  'repo:created',
1197:  -     'repo:deleted',
1198:  +     'repo:push',
1199:  'repo:updated',
1200:  }
1201:  ========================= 2 failed, 77 passed in 2.45s =========================
1202:  make: *** [../_infra/Makefile:74: test] Error 1
1203:  ##[error]Process completed with exit code 2.
1204:  ##[group]Run mikepenz/action-junit-report@v5
1205:  with:
1206:  report_paths: **/junit/test-results-**/*.xml
1207:  include_passed: true
1208:  require_tests: true
1209:  fail_on_failure: true
1210:  token: ***
1211:  group_reports: true
1212:  annotate_only: false
1213:  check_annotations: true
1214:  update_check: false
1215:  check_name: JUnit Test Report
1216:  fail_on_parse_error: false
1217:  require_passed_tests: false
...

1247:  ##[group]📦 Process test results
1248:  Preparing 1 report as configured.
1249:  test_folder:1 | test_find_matching_folders0.003s
1250:  test_rate_limiter:1 | test_cancellation0.204s
1251:  test_rate_limiter:1 | test_rolling_window_behavior0.305s
1252:  test_folder:1 | test_extract_repo_names_from_patterns0.003s
1253:  test_folder:1 | test_extract_repo_names_from_patterns_empty0.002s
1254:  test_folder:1 | test_extract_repo_names_from_patterns_no_repos0.002s
1255:  test_folder:1 | test_process_folder_patterns0.003s
1256:  test_folder:1 | test_process_repo_folders0.002s
1257:  test_folder:1 | test_create_pattern_mapping0.001s
1258:  test_rate_limiter:1 | test_basic_rate_limiting0.103s
1259:  test_rate_limiter:1 | test_purge_behavior0.302s
1260:  test_client:1 | test_send_api_request_success0.004s
1261:  test_client:1 | test_send_api_request_with_params0.003s
1262:  test_client:1 | test_send_api_request_error0.004s
1263:  test_client:1 | test_send_paginated_api_request0.003s
...

1292:  TestPullRequestWebhookProcessor:1 | test_validate_payload[payload3-False]0.002s
1293:  TestRepositoryWebhookProcessor:1 | test_should_process_event[repo:created-True]0.002s
1294:  TestRepositoryWebhookProcessor:1 | test_should_process_event[repo:updated-True]0.002s
1295:  TestRepositoryWebhookProcessor:1 | assert False == True0.002s
1296:  TestRepositoryWebhookProcessor:1 | test_should_process_event[pullrequest:created-False]0.002s
1297:  test_rate_limiter:1 | test_concurrent_requests0.203s
1298:  TestRepositoryWebhookProcessor:1 | test_should_process_event[pullrequest:updated-False]0.002s
1299:  TestRepositoryWebhookProcessor:1 | test_should_process_event[invalid:event-False]0.002s
1300:  test_rate_limiter:1 | test_edge_cases0.002s
1301:  TestRepositoryWebhookProcessor:1 | test_get_matching_kinds0.002s
1302:  TestRepositoryWebhookProcessor:1 | test_validate_payload[payload0-True]0.002s
1303:  test_rate_limiter:1 | test_wait_for_next_slot0.203s
1304:  TestRepositoryWebhookProcessor:1 | test_validate_payload[payload2-False]0.002s
1305:  TestPullRequestEvents:1 | test_values0.001s
1306:  TestPullRequestEvents:1 | test_enum_access0.001s
1307:  TestRepositoryEvents:1 | AssertionError: assert {'repo:created', 'repo:push', 'repo:updated'} == {'repo:created', 'repo:deleted', 'repo:updated'}0.001s
1308:  TestRepositoryEvents:1 | test_enum_access0.001s
...

1316:  TestBitbucketWebhookClient:1 | test_workspace_webhook_url0.001s
1317:  TestBitbucketWebhookClient:1 | test_webhook_exist_not_found0.002s
1318:  TestBitbucketWebhookClient:1 | test_webhook_exist_found0.002s
1319:  TestBitbucketWebhookClient:1 | test_create_webhook_when_not_exist0.002s
1320:  TestRepositoryWebhookProcessor:1 | test_handle_event0.049s
1321:  TestBitbucketWebhookClient:1 | test_create_webhook_when_already_exists0.002s
1322:  TestRepositoryWebhookProcessor:1 | test_validate_payload[payload1-False]0.003s
1323:  test_rate_limiter:1 | test_context_manager0.403s
1324:  test_client:1 | test_client_init_with_token0.026s
1325:  test_client:1 | test_client_init_with_app_password0.024s
1326:  test_client:1 | test_client_init_no_auth0.002s
1327:  test_rate_limiter:1 | test_metrics0.103s
1328:  ℹ️ Posting with conclusion 'failure' to https://github.com/port-labs/ocean/pull/1500 (sha: 066413ae8233d98e78b6f6d855e12bd0a23d08fa)
1329:  ##[endgroup]
1330:  ##[group]🚀 Publish results
1331:  ℹ️ - JUnit Test Report - 79 tests run, 77 passed, 0 skipped, 2 failed.
1332:  🧪 - TestRepositoryWebhookProcessor | assert False == True
1333:  🧪 - TestRepositoryEvents | AssertionError: assert {'repo:created', 'repo:push', 'repo:updated'} == {'repo:created', 'repo:deleted', 'repo:updated'}
1334:  ℹ️ - JUnit Test Report - Creating check (Annotations: 2)
1335:  ##[error]❌ Tests reported 2 failures
1336:  ##[endgroup]

```
</details></td></tr></table>
author_association: CONTRIBUTOR