Skip to content

Commit a6a05c8

Browse files
artem1205xiaohansong
authored andcommitted
Source Github: add integration tests (#34933)
1 parent 36c1c79 commit a6a05c8

File tree

12 files changed

+332
-59
lines changed

12 files changed

+332
-59
lines changed

airbyte-integrations/connectors/source-github/acceptance-test-config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ acceptance_tests:
3232
extra_records: yes
3333
empty_streams:
3434
- name: "events"
35-
bypass_reason: "Only events created within the past 90 days can be showed"
35+
bypass_reason: "Only events created within the past 90 days can be showed. Stream is tested with integration tests."
3636
ignored_fields:
3737
contributor_activity:
3838
- name: weeks

airbyte-integrations/connectors/source-github/metadata.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ data:
1010
connectorSubtype: api
1111
connectorType: source
1212
definitionId: ef69ef6e-aa7f-4af1-a01d-ef775033524e
13-
dockerImageTag: 1.6.1
13+
dockerImageTag: 1.6.2
1414
dockerRepository: airbyte/source-github
1515
documentationUrl: https://docs.airbyte.com/integrations/sources/github
1616
githubIssueLabel: source-github

airbyte-integrations/connectors/source-github/poetry.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

airbyte-integrations/connectors/source-github/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
33
build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
6-
version = "1.6.1"
6+
version = "1.6.2"
77
name = "source-github"
88
description = "Source implementation for Github."
99
authors = [ "Airbyte <[email protected]>",]
@@ -17,7 +17,7 @@ include = "source_github"
1717

1818
[tool.poetry.dependencies]
1919
python = "^3.9,<3.12"
20-
airbyte-cdk = "==0.60.1"
20+
airbyte-cdk = "^0.62.1"
2121
sgqlc = "==16.3"
2222

2323
[tool.poetry.scripts]

airbyte-integrations/connectors/source-github/unit_tests/conftest.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,8 @@
1212
def rate_limit_mock_response():
1313
rate_limit_response = {
1414
"resources": {
15-
"core": {
16-
"limit": 5000,
17-
"used": 0,
18-
"remaining": 5000,
19-
"reset": 4070908800
20-
},
21-
"graphql": {
22-
"limit": 5000,
23-
"used": 0,
24-
"remaining": 5000,
25-
"reset": 4070908800
26-
}
15+
"core": {"limit": 5000, "used": 0, "remaining": 5000, "reset": 4070908800},
16+
"graphql": {"limit": 5000, "used": 0, "remaining": 5000, "reset": 4070908800},
2717
}
2818
}
2919
responses.add(responses.GET, "https://api.github.com/rate_limit", json=rate_limit_response)

airbyte-integrations/connectors/source-github/unit_tests/integration/__init__.py

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2+
3+
from datetime import datetime
4+
from typing import Any, Dict, List
5+
6+
7+
class ConfigBuilder:
8+
def __init__(self) -> None:
9+
self._config: Dict[str, Any] = {
10+
"credentials": {"option_title": "PAT Credentials", "personal_access_token": "GITHUB_TEST_TOKEN"},
11+
"start_date": "2020-05-01T00:00:00Z",
12+
}
13+
14+
def with_repositories(self, repositories: List[str]) -> "ConfigBuilder":
15+
self._config["repositories"] = repositories
16+
return self
17+
18+
def with_client_secret(self, client_secret: str) -> "ConfigBuilder":
19+
self._config["client_secret"] = client_secret
20+
return self
21+
22+
def with_start_date(self, start_datetime: datetime) -> "ConfigBuilder":
23+
self._config["start_date"] = start_datetime.isoformat()[:-13] + "Z"
24+
return self
25+
26+
def with_branches(self, branches: List[str]) -> "ConfigBuilder":
27+
self._config["branches"] = branches
28+
return self
29+
30+
def with_api_url(self, api_url: str) -> "ConfigBuilder":
31+
self._config["api_url"] = api_url
32+
return self
33+
34+
def build(self) -> Dict[str, Any]:
35+
return self._config
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2+
3+
import json
4+
from unittest import TestCase
5+
6+
from airbyte_cdk.models import SyncMode
7+
from airbyte_cdk.test.catalog_builder import CatalogBuilder
8+
from airbyte_cdk.test.entrypoint_wrapper import read
9+
from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse
10+
from airbyte_cdk.test.mock_http.response_builder import find_template
11+
from airbyte_cdk.test.state_builder import StateBuilder
12+
from airbyte_protocol.models import AirbyteStreamStatus, Level, TraceType
13+
from source_github import SourceGithub
14+
15+
from .config import ConfigBuilder
16+
17+
_CONFIG = ConfigBuilder().with_repositories(["airbytehq/integration-test"]).build()
18+
19+
20+
def _create_catalog(sync_mode: SyncMode = SyncMode.full_refresh):
21+
return CatalogBuilder().with_stream(name="events", sync_mode=sync_mode).build()
22+
23+
24+
class EventsTest(TestCase):
25+
def setUp(self) -> None:
26+
"""Base setup for all tests. Add responses for:
27+
1. rate limit checker
28+
2. repositories
29+
3. branches
30+
"""
31+
32+
self.r_mock = HttpMocker()
33+
self.r_mock.__enter__()
34+
self.r_mock.get(
35+
HttpRequest(
36+
url="https://api.github.com/rate_limit",
37+
query_params={},
38+
headers={
39+
"Accept": "application/vnd.github+json",
40+
"X-GitHub-Api-Version": "2022-11-28",
41+
"Authorization": "token GITHUB_TEST_TOKEN",
42+
},
43+
),
44+
HttpResponse(
45+
json.dumps(
46+
{
47+
"resources": {
48+
"core": {"limit": 5000, "used": 0, "remaining": 5000, "reset": 5070908800},
49+
"graphql": {"limit": 5000, "used": 0, "remaining": 5000, "reset": 5070908800},
50+
}
51+
}
52+
),
53+
200,
54+
),
55+
)
56+
57+
self.r_mock.get(
58+
HttpRequest(
59+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}",
60+
query_params={"per_page": 100},
61+
),
62+
HttpResponse(json.dumps({"full_name": "airbytehq/integration-test", "default_branch": "master"}), 200),
63+
)
64+
65+
self.r_mock.get(
66+
HttpRequest(
67+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}/branches",
68+
query_params={"per_page": 100},
69+
),
70+
HttpResponse(json.dumps([{"repository": "airbytehq/integration-test", "name": "master"}]), 200),
71+
)
72+
73+
def teardown(self):
74+
"""Stops and resets HttpMocker instance."""
75+
self.r_mock.__exit__()
76+
77+
def test_read_full_refresh_no_pagination(self):
78+
"""Ensure http integration and record extraction"""
79+
self.r_mock.get(
80+
HttpRequest(
81+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}/events",
82+
query_params={"per_page": 100},
83+
),
84+
HttpResponse(json.dumps(find_template("events", __file__)), 200),
85+
)
86+
87+
source = SourceGithub()
88+
actual_messages = read(source, config=_CONFIG, catalog=_create_catalog())
89+
90+
assert len(actual_messages.records) == 2
91+
92+
def test_read_transformation(self):
93+
"""Ensure transformation applied to all records"""
94+
95+
self.r_mock.get(
96+
HttpRequest(
97+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}/events",
98+
query_params={"per_page": 100},
99+
),
100+
HttpResponse(json.dumps(find_template("events", __file__)), 200),
101+
)
102+
103+
source = SourceGithub()
104+
actual_messages = read(source, config=_CONFIG, catalog=_create_catalog())
105+
106+
assert len(actual_messages.records) == 2
107+
assert all(("repository", "airbytehq/integration-test") in x.record.data.items() for x in actual_messages.records)
108+
109+
def test_full_refresh_with_pagination(self):
110+
"""Ensure pagination"""
111+
self.r_mock.get(
112+
HttpRequest(
113+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}/events",
114+
query_params={"per_page": 100},
115+
),
116+
HttpResponse(
117+
body=json.dumps(find_template("events", __file__)),
118+
status_code=200,
119+
headers={"Link": '<https://api.github.com/repos/{}/events?page=2>; rel="next"'.format(_CONFIG.get("repositories")[0])},
120+
),
121+
)
122+
self.r_mock.get(
123+
HttpRequest(
124+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}/events",
125+
query_params={"per_page": 100, "page": 2},
126+
),
127+
HttpResponse(
128+
body=json.dumps(find_template("events", __file__)),
129+
status_code=200,
130+
),
131+
)
132+
source = SourceGithub()
133+
actual_messages = read(source, config=_CONFIG, catalog=_create_catalog())
134+
135+
assert len(actual_messages.records) == 4
136+
137+
def test_given_state_more_recent_than_some_records_when_read_incrementally_then_filter_records(self):
138+
"""Ensure incremental sync.
139+
Stream `Events` is semi-incremental, so all requests will be performed and only new records will be extracted"""
140+
141+
self.r_mock.get(
142+
HttpRequest(
143+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}/events",
144+
query_params={"per_page": 100},
145+
),
146+
HttpResponse(json.dumps(find_template("events", __file__)), 200),
147+
)
148+
149+
source = SourceGithub()
150+
actual_messages = read(
151+
source,
152+
config=_CONFIG,
153+
catalog=_create_catalog(sync_mode=SyncMode.incremental),
154+
state=StateBuilder()
155+
.with_stream_state("events", {"airbytehq/integration-test": {"created_at": "2022-06-09T10:00:00Z"}})
156+
.build(),
157+
)
158+
assert len(actual_messages.records) == 1
159+
160+
def test_when_read_incrementally_then_emit_state_message(self):
161+
"""Ensure incremental sync emits correct stream state message"""
162+
163+
self.r_mock.get(
164+
HttpRequest(
165+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}/events",
166+
query_params={"per_page": 100},
167+
),
168+
HttpResponse(json.dumps(find_template("events", __file__)), 200),
169+
)
170+
171+
source = SourceGithub()
172+
actual_messages = read(
173+
source,
174+
config=_CONFIG,
175+
catalog=_create_catalog(sync_mode=SyncMode.incremental),
176+
state=StateBuilder()
177+
.with_stream_state("events", {"airbytehq/integration-test": {"created_at": "2020-06-09T10:00:00Z"}})
178+
.build(),
179+
)
180+
assert actual_messages.state_messages[0].state.data == {'events': {'airbytehq/integration-test': {'created_at': '2022-06-09T12:47:28Z'}}}
181+
182+
def test_read_handles_expected_error_correctly_and_exits_with_complete_status(self):
183+
"""Ensure read() method does not raise an Exception and log message with error is in output"""
184+
self.r_mock.get(
185+
HttpRequest(
186+
url=f"https://api.github.com/repos/{_CONFIG.get('repositories')[0]}/events",
187+
query_params={"per_page": 100},
188+
),
189+
HttpResponse('{"message":"some_error_message"}', 403),
190+
)
191+
source = SourceGithub()
192+
actual_messages = read(source, config=_CONFIG, catalog=_create_catalog())
193+
194+
assert Level.ERROR in [x.log.level for x in actual_messages.logs]
195+
events_stream_complete_message = [x for x in actual_messages.trace_messages if x.trace.type == TraceType.STREAM_STATUS][-1]
196+
assert events_stream_complete_message.trace.stream_status.stream_descriptor.name == 'events'
197+
assert events_stream_complete_message.trace.stream_status.status == AirbyteStreamStatus.COMPLETE
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
[
2+
{
3+
"id": "22249084964",
4+
"type": "PushEvent",
5+
"actor": {
6+
"id": 583231,
7+
"login": "octocat",
8+
"display_login": "octocat",
9+
"gravatar_id": "",
10+
"url": "https://api.github.com/users/octocat",
11+
"avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4"
12+
},
13+
"repo": {
14+
"id": 1296269,
15+
"name": "octocat/Hello-World",
16+
"url": "https://api.github.com/repos/octocat/Hello-World"
17+
},
18+
"payload": {
19+
"push_id": 10115855396,
20+
"size": 1,
21+
"distinct_size": 1,
22+
"ref": "refs/heads/master",
23+
"head": "7a8f3ac80e2ad2f6842cb86f576d4bfe2c03e300",
24+
"before": "883efe034920928c47fe18598c01249d1a9fdabd",
25+
"commits": [
26+
{
27+
"sha": "7a8f3ac80e2ad2f6842cb86f576d4bfe2c03e300",
28+
"author": {
29+
"email": "[email protected]",
30+
"name": "Monalisa Octocat"
31+
},
32+
"message": "commit",
33+
"distinct": true,
34+
"url": "https://api.github.com/repos/octocat/Hello-World/commits/7a8f3ac80e2ad2f6842cb86f576d4bfe2c03e300"
35+
}
36+
]
37+
},
38+
"public": true,
39+
"created_at": "2022-06-09T12:47:28Z"
40+
},
41+
{
42+
"id": "22237752260",
43+
"type": "WatchEvent",
44+
"actor": {
45+
"id": 583231,
46+
"login": "octocat",
47+
"display_login": "octocat",
48+
"gravatar_id": "",
49+
"url": "https://api.github.com/users/octocat",
50+
"avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4"
51+
},
52+
"repo": {
53+
"id": 1296269,
54+
"name": "octocat/Hello-World",
55+
"url": "https://api.github.com/repos/octocat/Hello-World"
56+
},
57+
"payload": {
58+
"action": "started"
59+
},
60+
"public": true,
61+
"created_at": "2022-06-08T23:29:25Z"
62+
}
63+
]

0 commit comments

Comments
 (0)