Skip to content

Commit 103dd2a

Browse files
authored
Increased unit test coverage to 90 (#11317)
1 parent 1f98286 commit 103dd2a

File tree

4 files changed

+294
-36
lines changed

4 files changed

+294
-36
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#
2+
# Copyright (c) 2021 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import pytest
6+
from source_hubspot.source import SourceHubspot
7+
from source_hubspot.streams import API
8+
9+
NUMBER_OF_PROPERTIES = 2000
10+
11+
12+
@pytest.fixture(name="oauth_config")
13+
def oauth_config_fixture():
14+
return {
15+
"start_date": "2021-10-10T00:00:00Z",
16+
"credentials": {
17+
"credentials_title": "OAuth Credentials",
18+
"redirect_uri": "https://airbyte.io",
19+
"client_id": "test_client_id",
20+
"client_secret": "test_client_secret",
21+
"refresh_token": "test_refresh_token",
22+
"access_token": "test_access_token",
23+
"token_expires": "2021-05-30T06:00:00Z",
24+
},
25+
}
26+
27+
28+
@pytest.fixture(name="common_params")
29+
def common_params_fixture(config):
30+
source = SourceHubspot()
31+
common_params = source.get_common_params(config=config)
32+
return common_params
33+
34+
35+
@pytest.fixture(name="config")
36+
def config_fixture():
37+
return {"start_date": "2021-01-10T00:00:00Z", "credentials": {"credentials_title": "API Key Credentials", "api_key": "test_api_key"}}
38+
39+
40+
@pytest.fixture(name="some_credentials")
41+
def some_credentials_fixture():
42+
return {"credentials_title": "API Key Credentials", "api_key": "wrong_key"}
43+
44+
45+
@pytest.fixture(name="creds_with_wrong_permissions")
46+
def creds_with_wrong_permissions():
47+
return {"credentials_title": "API Key Credentials", "api_key": "THIS-IS-THE-API_KEY"}
48+
49+
50+
@pytest.fixture(name="fake_properties_list")
51+
def fake_properties_list():
52+
return [f"property_number_{i}" for i in range(NUMBER_OF_PROPERTIES)]
53+
54+
55+
@pytest.fixture(name="api")
56+
def api(some_credentials):
57+
return API(some_credentials)

airbyte-integrations/connectors/source-hubspot/unit_tests/test_source.py

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,96 @@
44

55

66
import logging
7+
from http import HTTPStatus
8+
from unittest.mock import MagicMock
79

10+
import pendulum
811
import pytest
912
from airbyte_cdk.models import ConfiguredAirbyteCatalog, SyncMode, Type
13+
from source_hubspot.errors import HubspotRateLimited
1014
from source_hubspot.source import SourceHubspot
11-
from source_hubspot.streams import API, PROPERTIES_PARAM_MAX_LENGTH, Companies, Deals, Products, Workflows, split_properties
15+
from source_hubspot.streams import API, PROPERTIES_PARAM_MAX_LENGTH, Companies, Deals, Products, Stream, Workflows, split_properties
1216

1317
NUMBER_OF_PROPERTIES = 2000
1418

1519
logger = logging.getLogger("test_client")
1620

1721

18-
@pytest.fixture(name="oauth_config")
19-
def oauth_config_fixture():
20-
return {
21-
"start_date": "2021-10-10T00:00:00Z",
22-
"credentials": {
23-
"credentials_title": "OAuth Credentials",
24-
"redirect_uri": "https://airbyte.io",
25-
"client_id": "test_client_id",
26-
"client_secret": "test_client_secret",
27-
"refresh_token": "test_refresh_token",
28-
"access_token": "test_access_token",
29-
"token_expires": "2021-05-30T06:00:00Z",
30-
},
31-
}
22+
def test_check_connection_ok(requests_mock, config):
23+
responses = [
24+
{"json": [], "status_code": 200},
25+
]
26+
27+
requests_mock.register_uri("GET", "/properties/v2/contact/properties", responses)
28+
ok, error_msg = SourceHubspot().check_connection(logger, config=config)
3229

30+
assert ok
31+
assert not error_msg
3332

34-
@pytest.fixture(name="common_params")
35-
def common_params_fixture(config):
36-
source = SourceHubspot()
37-
common_params = source.get_common_params(config=config)
38-
return common_params
33+
34+
def test_check_connection_empty_config(config):
35+
config = {}
36+
37+
with pytest.raises(KeyError):
38+
SourceHubspot().check_connection(logger, config=config)
39+
40+
41+
def test_check_connection_invalid_config(config):
42+
config.pop("start_date")
43+
44+
with pytest.raises(TypeError):
45+
SourceHubspot().check_connection(logger, config=config)
46+
47+
48+
def test_check_connection_exception(config):
49+
ok, error_msg = SourceHubspot().check_connection(logger, config=config)
50+
51+
assert not ok
52+
assert error_msg
3953

4054

41-
@pytest.fixture(name="config")
42-
def config_fixture():
43-
return {"start_date": "2021-01-10T00:00:00Z", "credentials": {"credentials_title": "API Key Credentials", "api_key": "test_api_key"}}
55+
def test_streams(config):
56+
streams = SourceHubspot().streams(config)
4457

58+
assert len(streams) == 27
4559

46-
@pytest.fixture(name="some_credentials")
47-
def some_credentials_fixture():
48-
return {"credentials_title": "API Key Credentials", "api_key": "wrong_key"}
4960

61+
def test_check_credential_title_exception(config):
62+
config["credentials"].pop("credentials_title")
5063

51-
@pytest.fixture(name="creds_with_wrong_permissions")
52-
def creds_with_wrong_permissions():
53-
return {"credentials_title": "API Key Credentials", "api_key": "THIS-IS-THE-API_KEY"}
64+
with pytest.raises(Exception):
65+
SourceHubspot().check_connection(logger, config=config)
5466

5567

56-
@pytest.fixture(name="fake_properties_list")
57-
def fake_properties_list():
58-
return [f"property_number_{i}" for i in range(NUMBER_OF_PROPERTIES)]
68+
def test_parse_and_handle_errors(some_credentials):
69+
response = MagicMock()
70+
response.status_code = HTTPStatus.TOO_MANY_REQUESTS
71+
72+
with pytest.raises(HubspotRateLimited):
73+
API(some_credentials)._parse_and_handle_errors(response)
74+
75+
76+
def test_convert_datetime_to_string():
77+
pendulum_time = pendulum.now()
78+
79+
assert Stream._convert_datetime_to_string(pendulum_time, declared_format="date")
80+
assert Stream._convert_datetime_to_string(pendulum_time, declared_format="date-time")
81+
82+
83+
def test_cast_datetime(common_params, caplog):
84+
field_value = pendulum.now()
85+
field_name = "curent_time"
86+
87+
Companies(**common_params)._cast_datetime(field_name, field_value)
88+
89+
expected_warining_message = {
90+
"type": "LOG",
91+
"log": {
92+
"level": "WARN",
93+
"message": f"Couldn't parse date/datetime string in {field_name}, trying to parse timestamp... Field value: {field_value}. Ex: argument of type 'DateTime' is not iterable",
94+
},
95+
}
96+
assert expected_warining_message["log"]["message"] in caplog.text
5997

6098

6199
def test_check_connection_backoff_on_limit_reached(requests_mock, config):
@@ -132,10 +170,6 @@ class TestSplittingPropertiesFunctionality:
132170
"archived": False,
133171
}
134172

135-
@pytest.fixture
136-
def api(self, some_credentials):
137-
return API(some_credentials)
138-
139173
@staticmethod
140174
def set_mock_properties(requests_mock, url, fake_properties_list):
141175
properties_response = [
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
#
2+
# Copyright (c) 2021 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
import pytest
6+
from source_hubspot.streams import (
7+
Campaigns,
8+
Companies,
9+
ContactLists,
10+
Contacts,
11+
DealPipelines,
12+
Deals,
13+
EmailEvents,
14+
EngagementsCalls,
15+
EngagementsEmails,
16+
EngagementsMeetings,
17+
EngagementsNotes,
18+
EngagementsTasks,
19+
FeedbackSubmissions,
20+
Forms,
21+
FormSubmissions,
22+
LineItems,
23+
MarketingEmails,
24+
Owners,
25+
Products,
26+
Quotes,
27+
TicketPipelines,
28+
Tickets,
29+
Workflows,
30+
)
31+
32+
from .utils import read_full_refresh, read_incremental
33+
34+
35+
@pytest.mark.parametrize(
36+
"stream, endpoint",
37+
[
38+
(Campaigns, "campaigns"),
39+
(Companies, "company"),
40+
(ContactLists, "contact"),
41+
(Contacts, "contact"),
42+
(Deals, "deal"),
43+
(DealPipelines, "deal"),
44+
(Quotes, "quote"),
45+
(EmailEvents, ""),
46+
(EngagementsCalls, "calls"),
47+
(EngagementsEmails, "emails"),
48+
(EngagementsMeetings, "meetings"),
49+
(EngagementsNotes, "notes"),
50+
(EngagementsTasks, "tasks"),
51+
(FeedbackSubmissions, "feedback_submissions"),
52+
(Forms, "form"),
53+
(FormSubmissions, "form"),
54+
(LineItems, "line_item"),
55+
(MarketingEmails, ""),
56+
(Owners, ""),
57+
(Products, "product"),
58+
(Quotes, "quote"),
59+
(TicketPipelines, ""),
60+
(Tickets, "ticket"),
61+
(Workflows, ""),
62+
],
63+
)
64+
def test_streams_read(stream, endpoint, requests_mock, common_params, fake_properties_list):
65+
stream = stream(**common_params)
66+
responses = [
67+
{
68+
"json": {
69+
stream.data_field: [
70+
{
71+
"id": "test_id",
72+
"created": "2022-02-25T16:43:11Z",
73+
"updatedAt": "2022-02-25T16:43:11Z",
74+
"lastUpdatedTime": "2022-02-25T16:43:11Z",
75+
}
76+
],
77+
}
78+
}
79+
]
80+
properties_response = [
81+
{
82+
"json": [
83+
{"name": property_name, "type": "string", "updatedAt": 1571085954360, "createdAt": 1565059306048}
84+
for property_name in fake_properties_list
85+
],
86+
"status_code": 200,
87+
}
88+
]
89+
is_form_submission = isinstance(stream, FormSubmissions)
90+
stream_url = stream.url + "/test_id" if is_form_submission else stream.url
91+
92+
requests_mock.register_uri("GET", stream_url, responses)
93+
requests_mock.register_uri("GET", "/marketing/v3/forms", responses)
94+
requests_mock.register_uri("GET", "/email/public/v1/campaigns/test_id", responses)
95+
requests_mock.register_uri("GET", f"/properties/v2/{endpoint}/properties", properties_response)
96+
97+
records = read_incremental(stream, {})
98+
99+
assert records
100+
101+
102+
@pytest.mark.parametrize(
103+
"error_response",
104+
[
105+
{"json": {}, "status_code": 429},
106+
{"json": {}, "status_code": 502},
107+
{"json": {}, "status_code": 504},
108+
],
109+
)
110+
def test_common_error_retry(error_response, requests_mock, common_params, fake_properties_list):
111+
"""Error once, check that we retry and not fail"""
112+
properties_response = [
113+
{"name": property_name, "type": "string", "updatedAt": 1571085954360, "createdAt": 1565059306048}
114+
for property_name in fake_properties_list
115+
]
116+
responses = [
117+
error_response,
118+
{
119+
"json": properties_response,
120+
"status_code": 200,
121+
},
122+
]
123+
124+
stream = Companies(**common_params)
125+
126+
response = {
127+
stream.data_field: [
128+
{
129+
"id": "test_id",
130+
"created": "2022-02-25T16:43:11Z",
131+
"updatedAt": "2022-02-25T16:43:11Z",
132+
"lastUpdatedTime": "2022-02-25T16:43:11Z",
133+
}
134+
],
135+
}
136+
requests_mock.register_uri("GET", "/properties/v2/company/properties", responses)
137+
requests_mock.register_uri("GET", stream.url, [{"json": response}])
138+
records = read_full_refresh(stream)
139+
140+
assert [response[stream.data_field][0]] == records
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#
2+
# Copyright (c) 2021 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
from typing import Any, MutableMapping
6+
7+
from airbyte_cdk.models import SyncMode
8+
from airbyte_cdk.sources.streams import Stream
9+
10+
11+
def read_incremental(stream_instance: Stream, stream_state: MutableMapping[str, Any]):
12+
res = []
13+
slices = stream_instance.stream_slices(sync_mode=SyncMode.incremental, stream_state=stream_state)
14+
for slice in slices:
15+
records = stream_instance.read_records(sync_mode=SyncMode.incremental, stream_slice=slice, stream_state=stream_state)
16+
for record in records:
17+
stream_state = stream_instance.get_updated_state(stream_state, record)
18+
res.append(record)
19+
return res
20+
21+
22+
def read_full_refresh(stream_instance: Stream):
23+
records = []
24+
slices = stream_instance.stream_slices(sync_mode=SyncMode.full_refresh)
25+
for slice in slices:
26+
records.extend(list(stream_instance.read_records(stream_slice=slice, sync_mode=SyncMode.full_refresh)))
27+
return records

0 commit comments

Comments
 (0)