Skip to content

Commit 9af4ef7

Browse files
darynaishchenkojatinyadav-cc
authored andcommitted
🏥 Source Gitlab: increase test coverage, update Groups, Commits and Projects schemas. (airbytehq#33676)
1 parent 72d8e91 commit 9af4ef7

File tree

15 files changed

+271
-93
lines changed

15 files changed

+271
-93
lines changed

airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records.jsonl

+17-17
Large diffs are not rendered by default.

airbyte-integrations/connectors/source-gitlab/integration_tests/expected_records_with_ids.jsonl

+17-17
Large diffs are not rendered by default.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ data:
1010
connectorSubtype: api
1111
connectorType: source
1212
definitionId: 5e6175e5-68e1-4c17-bff9-56103bbb0d80
13-
dockerImageTag: 2.0.0
13+
dockerImageTag: 2.1.0
1414
dockerRepository: airbyte/source-gitlab
1515
documentationUrl: https://docs.airbyte.com/integrations/sources/gitlab
1616
githubIssueLabel: source-gitlab

airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/commits.json

+11
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@
3737
"type": ["null", "string"],
3838
"format": "date-time"
3939
},
40+
"extended_trailers": {
41+
"type": ["null", "object"],
42+
"properties": {
43+
"Cc": {
44+
"type": ["null", "array"],
45+
"items": {
46+
"type": ["null", "string"]
47+
}
48+
}
49+
}
50+
},
4051
"committer_name": {
4152
"type": ["null", "string"]
4253
},

airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/groups.json

+9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"id": {
2020
"type": ["null", "integer"]
2121
},
22+
"organization_id": {
23+
"type": ["null", "integer"]
24+
},
2225
"default_branch_protection_defaults": {
2326
"type": ["null", "object"],
2427
"properties": {
@@ -74,6 +77,9 @@
7477
"emails_disabled": {
7578
"type": ["null", "boolean"]
7679
},
80+
"emails_enabled": {
81+
"type": ["null", "boolean"]
82+
},
7783
"mentions_disabled": {
7884
"type": ["null", "boolean"]
7985
},
@@ -144,6 +150,9 @@
144150
},
145151
"shared_runners_setting": {
146152
"type": ["null", "string"]
153+
},
154+
"service_access_tokens_expiration_enforced": {
155+
"type": ["null", "boolean"]
147156
}
148157
}
149158
}

airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/issues.json

+17-14
Original file line numberDiff line numberDiff line change
@@ -235,20 +235,23 @@
235235
}
236236
},
237237
"epic": {
238-
"id": {
239-
"type": ["null", "integer"]
240-
},
241-
"iid": {
242-
"type": ["null", "integer"]
243-
},
244-
"title": {
245-
"type": ["null", "string"]
246-
},
247-
"url": {
248-
"type": ["null", "string"]
249-
},
250-
"group_id": {
251-
"type": ["null", "integer"]
238+
"type": ["null", "object"],
239+
"properties": {
240+
"id": {
241+
"type": ["null", "integer"]
242+
},
243+
"iid": {
244+
"type": ["null", "integer"]
245+
},
246+
"title": {
247+
"type": ["null", "string"]
248+
},
249+
"url": {
250+
"type": ["null", "string"]
251+
},
252+
"group_id": {
253+
"type": ["null", "integer"]
254+
}
252255
}
253256
},
254257
"epic_iid": {

airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/jobs.json

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
"stage": {
1212
"type": ["null", "string"]
1313
},
14+
"archived": {
15+
"type": ["null", "boolean"]
16+
},
1417
"name": {
1518
"type": ["null", "string"]
1619
},

airbyte-integrations/connectors/source-gitlab/source_gitlab/schemas/projects.json

+9
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,15 @@
483483
},
484484
"merge_trains_skip_train_allowed": {
485485
"type": ["null", "boolean"]
486+
},
487+
"code_suggestions": {
488+
"type": ["null", "boolean"]
489+
},
490+
"model_registry_access_level": {
491+
"type": ["null", "string"]
492+
},
493+
"ci_restrict_pipeline_cancellation_role": {
494+
"type": ["null", "string"]
486495
}
487496
}
488497
}

airbyte-integrations/connectors/source-gitlab/source_gitlab/streams.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapp
9292
elif isinstance(response_data, dict):
9393
yield self.transform(response_data, **kwargs)
9494
else:
95-
Exception(f"Unsupported type of response data for stream {self.name}")
95+
self.logger.info(f"Unsupported type of response data for stream {self.name}")
9696

9797
def transform(self, record: Dict[str, Any], stream_slice: Mapping[str, Any] = None, **kwargs):
9898
for key in self.flatten_id_keys:
@@ -166,7 +166,7 @@ def get_updated_state(self, current_stream_state: MutableMapping[str, Any], late
166166
current_state = current_state.get(self.cursor_field)
167167
current_state_value = current_state or latest_cursor_value
168168
max_value = max(pendulum.parse(current_state_value), pendulum.parse(latest_cursor_value))
169-
current_stream_state[str(project_id)] = {self.cursor_field: str(max_value)}
169+
current_stream_state[str(project_id)] = {self.cursor_field: max_value.to_iso8601_string()}
170170
return current_stream_state
171171

172172
@staticmethod
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "groups": "a b c", "groups_list": ["a", "c", "b"] }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2+
3+
import os
4+
5+
from source_gitlab.config_migrations import MigrateGroups
6+
from source_gitlab.source import SourceGitlab
7+
8+
TEST_CONFIG_PATH = f"{os.path.dirname(__file__)}/test_config.json"
9+
10+
11+
def test_should_migrate():
12+
assert MigrateGroups._should_migrate({"groups": "group group2 group3"}) is True
13+
assert MigrateGroups._should_migrate({"groups_list": ["test", "group2", "group3"]}) is False
14+
15+
16+
def test__modify_and_save():
17+
source = SourceGitlab()
18+
expected = {"groups": "a b c", "groups_list": ["b", "c", "a"]}
19+
modified_config = MigrateGroups._modify_and_save(config_path=TEST_CONFIG_PATH, source=source, config={"groups": "a b c"})
20+
assert modified_config["groups_list"].sort() == expected["groups_list"].sort()
21+
assert modified_config.get("groups")

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

+53-2
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,39 @@ def test_connection_fail_due_to_api_error(errror_code, expected_status, config,
6464
assert msg.startswith("Unable to connect to Gitlab API with the provided Private Access Token")
6565

6666

67+
def test_connection_fail_due_to_api_error_oauth(oauth_config, mocker, requests_mock):
68+
mocker.patch("time.sleep")
69+
test_response = {
70+
"access_token": "new_access_token",
71+
"expires_in": 7200,
72+
"created_at": 1735689600,
73+
# (7200 + 1735689600).timestamp().to_rfc3339_string() = "2025-01-01T02:00:00+00:00"
74+
"refresh_token": "new_refresh_token",
75+
}
76+
requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json=test_response)
77+
requests_mock.get("/api/v4/groups", status_code=500)
78+
source = SourceGitlab()
79+
status, msg = source.check_connection(logging.getLogger(), oauth_config)
80+
assert status is False
81+
assert msg.startswith("Unable to connect to Gitlab API with the provided credentials")
82+
83+
6784
def test_connection_fail_due_to_expired_access_token_error(oauth_config, requests_mock):
68-
expected = "Unable to refresh the `access_token`, please re-auth in Source > Settings."
85+
expected = "Unable to refresh the `access_token`, please re-authenticate in Sources > Settings."
6986
requests_mock.post("https://gitlab.com/oauth/token", status_code=401)
7087
source = SourceGitlab()
7188
status, msg = source.check_connection(logging.getLogger("airbyte"), oauth_config)
72-
assert status is False, expected in msg
89+
assert status is False
90+
assert expected in msg
91+
92+
93+
def test_connection_refresh_access_token(oauth_config, requests_mock):
94+
expected = "Unknown error occurred while checking the connection"
95+
requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json={"access_token": "new access token"})
96+
source = SourceGitlab()
97+
status, msg = source.check_connection(logging.getLogger("airbyte"), oauth_config)
98+
assert status is False
99+
assert expected in msg
73100

74101

75102
def test_refresh_expired_access_token_on_error(oauth_config, requests_mock):
@@ -108,3 +135,27 @@ def test_connection_fail_due_to_config_error(mocker, api_url, deployment_env, ex
108135
}
109136
status, msg = source.check_connection(logging.getLogger(), config)
110137
assert (status, msg) == (False, expected_message)
138+
139+
140+
def test_try_refresh_access_token(oauth_config, requests_mock):
141+
test_response = {
142+
"access_token": "new_access_token",
143+
"expires_in": 7200,
144+
"created_at": 1735689600,
145+
# (7200 + 1735689600).timestamp().to_rfc3339_string() = "2025-01-01T02:00:00+00:00"
146+
"refresh_token": "new_refresh_token",
147+
}
148+
requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json=test_response)
149+
150+
expected = {"api_url": "gitlab.com",
151+
"credentials": {"access_token": "new_access_token",
152+
"auth_type": "oauth2.0",
153+
"client_id": "client_id",
154+
"client_secret": "client_secret",
155+
"refresh_token": "new_refresh_token",
156+
"token_expiry_date": "2025-01-01T02:00:00+00:00"},
157+
"start_date": "2021-01-01T00:00:00Z"}
158+
159+
source = SourceGitlab()
160+
source._auth_params(oauth_config)
161+
assert source._try_refresh_access_token(logger=logging.getLogger(), config=oauth_config) == expected

airbyte-integrations/connectors/source-gitlab/unit_tests/test_streams.py

+52
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
#
44

55
import datetime
6+
from unittest.mock import MagicMock
67

78
import pytest
9+
from airbyte_cdk.models import SyncMode
810
from airbyte_cdk.sources.streams.http.auth import NoAuth
911
from source_gitlab.streams import (
1012
Branches,
@@ -276,3 +278,53 @@ def test_transform(requests_mock, stream, response_mocks, expected_records, requ
276278
def test_updated_state(stream, current_state, latest_record, new_state, request):
277279
stream = request.getfixturevalue(stream)
278280
assert stream.get_updated_state(current_state, latest_record) == new_state
281+
282+
283+
def test_parse_response_unsuported_response_type(request, caplog):
284+
stream = request.getfixturevalue("pipelines")
285+
from unittest.mock import MagicMock
286+
response = MagicMock()
287+
response.status_code = 200
288+
response.json = MagicMock(return_value="")
289+
list(stream.parse_response(response=response))
290+
assert "Unsupported type of response data for stream pipelines" in caplog.text
291+
292+
293+
def test_stream_slices_child_stream(request, requests_mock):
294+
commits = request.getfixturevalue("commits")
295+
requests_mock.get("https://gitlab.com/api/v4/projects/p_1?per_page=50&statistics=1",
296+
json=[{"id": 13082000, "description": "", "name": "New CI Test Project"}])
297+
298+
slices = list(commits.stream_slices(sync_mode=SyncMode.full_refresh, stream_state={"13082000": {""'created_at': "2021-03-10T23:58:1213"}}))
299+
assert slices
300+
301+
302+
def test_next_page_token(request):
303+
response = MagicMock()
304+
response.status_code = 200
305+
response.json = MagicMock(return_value=["some data"])
306+
commits = request.getfixturevalue("commits")
307+
assert not commits.next_page_token(response)
308+
data = ["some data" for x in range(0, 50)]
309+
response.json = MagicMock(return_value=data)
310+
assert commits.next_page_token(response) == {'page': 2}
311+
response.json = MagicMock(return_value={"data": "some data"})
312+
assert not commits.next_page_token(response)
313+
314+
315+
def test_availability_strategy(request):
316+
commits = request.getfixturevalue("commits")
317+
assert not commits.availability_strategy
318+
319+
320+
def test_request_params(request):
321+
commits = request.getfixturevalue("commits")
322+
expected = {'per_page': 50, 'page': 2, 'with_stats': True}
323+
assert commits.request_params(stream_slice={"updated_after": "2021-03-10T23:58:1213"}, next_page_token={'page': 2}) == expected
324+
325+
326+
def test_chunk_date_range(request):
327+
commits = request.getfixturevalue("commits")
328+
# start point in future
329+
start_point = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1)
330+
assert not list(commits._chunk_date_range(start_point))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
2+
3+
import pytest
4+
from source_gitlab.utils import parse_url
5+
6+
7+
@pytest.mark.parametrize(
8+
"url, expected",
9+
(
10+
("http://example.com", (True, "http", "example.com")),
11+
("http://example", (True, "http", "example")),
12+
("test://example.com", (False, "", "")),
13+
("https://example.com/test/test2", (False, "", "")),
14+
)
15+
)
16+
def test_parse_url(url, expected):
17+
assert parse_url(url) == expected

0 commit comments

Comments
 (0)