-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Source Hubspot: add integration tests (#35945)
- Loading branch information
Showing
50 changed files
with
2,300 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 28 additions & 2 deletions
30
airbyte-integrations/connectors/source-hubspot/poetry.lock
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",] | |
build-backend = "poetry.core.masonry.api" | ||
|
||
[tool.poetry] | ||
version = "4.1.0" | ||
version = "4.1.1" | ||
name = "source-hubspot" | ||
description = "Source implementation for HubSpot." | ||
authors = [ "Airbyte <[email protected]>",] | ||
|
@@ -27,3 +27,5 @@ requests-mock = "^1.9.3" | |
mock = "^5.1.0" | ||
pytest-mock = "^3.6" | ||
pytest = "^6.2" | ||
pytz = "2024.1" | ||
freezegun = "0.3.4" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
133 changes: 133 additions & 0 deletions
133
airbyte-integrations/connectors/source-hubspot/unit_tests/integrations/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
# Copyright (c) 2023 Airbyte, Inc., all rights reserved. | ||
|
||
import copy | ||
from datetime import datetime, timedelta | ||
from typing import Any, Dict, List, Optional | ||
|
||
import freezegun | ||
import pytz | ||
from airbyte_cdk.test.catalog_builder import CatalogBuilder | ||
from airbyte_cdk.test.entrypoint_wrapper import EntrypointOutput, read | ||
from airbyte_cdk.test.mock_http import HttpMocker | ||
from airbyte_cdk.test.mock_http.response_builder import FieldPath, HttpResponseBuilder, RecordBuilder, create_record_builder, find_template | ||
from airbyte_protocol.models import AirbyteStateMessage, SyncMode | ||
from source_hubspot import SourceHubspot | ||
|
||
from .config_builder import ConfigBuilder | ||
from .request_builders.api import CustomObjectsRequestBuilder, OAuthRequestBuilder, PropertiesRequestBuilder, ScopesRequestBuilder | ||
from .request_builders.streams import CRMStreamRequestBuilder, IncrementalCRMStreamRequestBuilder, WebAnalyticsRequestBuilder | ||
from .response_builder.helpers import RootHttpResponseBuilder | ||
from .response_builder.api import ScopesResponseBuilder | ||
from .response_builder.streams import GenericResponseBuilder, HubspotStreamResponseBuilder | ||
|
||
|
||
@freezegun.freeze_time("2024-03-03T14:42:00Z") | ||
class HubspotTestCase: | ||
DT_FORMAT = '%Y-%m-%dT%H:%M:%SZ' | ||
OBJECT_ID = "testID" | ||
ACCESS_TOKEN = "new_access_token" | ||
CURSOR_FIELD = "occurredAt" | ||
PROPERTIES = { | ||
"closed_date": "datetime", | ||
"createdate": "datetime", | ||
} | ||
|
||
@classmethod | ||
def now(cls): | ||
return datetime.now(pytz.utc) | ||
|
||
@classmethod | ||
def start_date(cls): | ||
return cls.now() - timedelta(days=30) | ||
|
||
@classmethod | ||
def updated_at(cls): | ||
return cls.now() - timedelta(days=1) | ||
|
||
@classmethod | ||
def dt_str(cls, dt: datetime.date) -> str: | ||
return dt.strftime(cls.DT_FORMAT) | ||
|
||
@classmethod | ||
def oauth_config(cls, start_date: Optional[str] = None) -> Dict[str, Any]: | ||
start_date = start_date or cls.dt_str(cls.start_date()) | ||
return ConfigBuilder().with_start_date(start_date).with_auth( | ||
{ | ||
"credentials_title": "OAuth Credentials", | ||
"redirect_uri": "https://airbyte.io", | ||
"client_id": "client_id", | ||
"client_secret": "client_secret", | ||
"refresh_token": "refresh_token", | ||
} | ||
).build() | ||
|
||
@classmethod | ||
def private_token_config(cls, token: str, start_date: Optional[str] = None) -> Dict[str, Any]: | ||
start_date = start_date or cls.dt_str(cls.start_date()) | ||
return ConfigBuilder().with_start_date(start_date).with_auth( | ||
{ | ||
"credentials_title": "Private App Credentials", | ||
"access_token": token, | ||
} | ||
).build() | ||
|
||
@classmethod | ||
def mock_oauth(cls, http_mocker: HttpMocker, token: str): | ||
creds = cls.oauth_config()["credentials"] | ||
req = OAuthRequestBuilder().with_client_id( | ||
creds["client_id"] | ||
).with_client_secret( | ||
creds["client_secret"] | ||
).with_refresh_token( | ||
creds["refresh_token"] | ||
).build() | ||
response = GenericResponseBuilder().with_value("access_token", token).with_value("expires_in", 7200).build() | ||
http_mocker.post(req, response) | ||
|
||
@classmethod | ||
def mock_scopes(cls, http_mocker: HttpMocker, token: str, scopes: List[str]): | ||
http_mocker.get(ScopesRequestBuilder().with_access_token(token).build(), ScopesResponseBuilder(scopes).build()) | ||
|
||
@classmethod | ||
def mock_custom_objects(cls, http_mocker: HttpMocker): | ||
http_mocker.get( | ||
CustomObjectsRequestBuilder().build(), | ||
HttpResponseBuilder({}, records_path=FieldPath("results"), pagination_strategy=None).build() | ||
) | ||
|
||
@classmethod | ||
def mock_properties(cls, http_mocker: HttpMocker, object_type: str, properties: Dict[str, str]): | ||
templates = find_template("properties", __file__) | ||
record_builder = lambda: RecordBuilder(copy.deepcopy(templates[0]), id_path=None, cursor_path=None) | ||
|
||
response_builder = RootHttpResponseBuilder(templates) | ||
for name, type in properties.items(): | ||
record = record_builder().with_field(FieldPath("name"), name).with_field(FieldPath("type"), type) | ||
response_builder = response_builder.with_record(record) | ||
|
||
http_mocker.get( | ||
PropertiesRequestBuilder().for_entity(object_type).build(), | ||
response_builder.build() | ||
) | ||
|
||
@classmethod | ||
def mock_response(cls, http_mocker: HttpMocker, request, responses, method: str = "get"): | ||
if not isinstance(responses, (list, tuple)): | ||
responses = [responses] | ||
getattr(http_mocker, method)(request, responses) | ||
|
||
@classmethod | ||
def record_builder(cls, stream: str, record_cursor_path): | ||
return create_record_builder( | ||
find_template(stream, __file__), records_path=FieldPath("results"), record_id_path=None, record_cursor_path=record_cursor_path | ||
) | ||
|
||
@classmethod | ||
def catalog(cls, stream: str, sync_mode: SyncMode): | ||
return CatalogBuilder().with_stream(stream, sync_mode).build() | ||
|
||
@classmethod | ||
def read_from_stream( | ||
cls, cfg, stream: str, sync_mode: SyncMode, state: Optional[List[AirbyteStateMessage]] = None, expecting_exception: bool = False | ||
) -> EntrypointOutput: | ||
return read(SourceHubspot(), cfg, cls.catalog(stream, sync_mode), state, expecting_exception) |
21 changes: 21 additions & 0 deletions
21
airbyte-integrations/connectors/source-hubspot/unit_tests/integrations/config_builder.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Copyright (c) 2023 Airbyte, Inc., all rights reserved. | ||
|
||
from typing import Any, Mapping | ||
|
||
|
||
class ConfigBuilder: | ||
def __init__(self): | ||
self._config = { | ||
"enable_experimental_streams": True | ||
} | ||
|
||
def with_start_date(self, start_date: str): | ||
self._config["start_date"] = start_date | ||
return self | ||
|
||
def with_auth(self, credentials: Mapping[str, str]): | ||
self._config["credentials"] = credentials | ||
return self | ||
|
||
def build(self) -> Mapping[str, Any]: | ||
return self._config |
9 changes: 9 additions & 0 deletions
9
...tegrations/connectors/source-hubspot/unit_tests/integrations/request_builders/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# Copyright (c) 2023 Airbyte, Inc., all rights reserved. | ||
|
||
import abc | ||
|
||
|
||
class AbstractRequestBuilder: | ||
@abc.abstractmethod | ||
def build(self): | ||
pass |
66 changes: 66 additions & 0 deletions
66
...te-integrations/connectors/source-hubspot/unit_tests/integrations/request_builders/api.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# Copyright (c) 2023 Airbyte, Inc., all rights reserved. | ||
|
||
from airbyte_cdk.test.mock_http import HttpRequest | ||
|
||
from . import AbstractRequestBuilder | ||
|
||
|
||
class OAuthRequestBuilder(AbstractRequestBuilder): | ||
URL = "https://api.hubapi.com/oauth/v1/token" | ||
|
||
def __init__(self): | ||
self._params = {} | ||
|
||
def with_client_id(self, client_id: str): | ||
self._params["client_id"] = client_id | ||
return self | ||
|
||
def with_client_secret(self, client_secret: str): | ||
self._params["client_secret"] = client_secret | ||
return self | ||
|
||
def with_refresh_token(self, refresh_token: str): | ||
self._params["refresh_token"] = refresh_token | ||
return self | ||
|
||
def build(self) -> HttpRequest: | ||
client_id, client_secret, refresh_token = self._params["client_id"], self._params["client_secret"], self._params["refresh_token"] | ||
return HttpRequest( | ||
url=self.URL, | ||
body=f"grant_type=refresh_token&client_id={client_id}&client_secret={client_secret}&refresh_token={refresh_token}" | ||
) | ||
|
||
|
||
class ScopesRequestBuilder(AbstractRequestBuilder): | ||
URL = "https://api.hubapi.com/oauth/v1/access-tokens/{token}" | ||
|
||
def __init__(self): | ||
self._token = None | ||
|
||
def with_access_token(self, token: str): | ||
self._token = token | ||
return self | ||
|
||
def build(self) -> HttpRequest: | ||
return HttpRequest(url=self.URL.format(token=self._token)) | ||
|
||
|
||
class CustomObjectsRequestBuilder(AbstractRequestBuilder): | ||
URL = "https://api.hubapi.com/crm/v3/schemas" | ||
|
||
def build(self) -> HttpRequest: | ||
return HttpRequest(url=self.URL) | ||
|
||
|
||
class PropertiesRequestBuilder(AbstractRequestBuilder): | ||
URL = "https://api.hubapi.com/properties/v2/{resource}/properties" | ||
|
||
def __init__(self): | ||
self._resource = None | ||
|
||
def for_entity(self, entity): | ||
self._resource = entity | ||
return self | ||
|
||
def build(self) -> HttpRequest: | ||
return HttpRequest(url=self.URL.format(resource=self._resource)) |
Oops, something went wrong.