Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🏥 Source Gitlab: increase test coverage, update Groups, Commits and Projects schemas. #33676

Merged
merged 14 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: 5e6175e5-68e1-4c17-bff9-56103bbb0d80
dockerImageTag: 2.0.0
dockerImageTag: 2.1.0
dockerRepository: airbyte/source-gitlab
documentationUrl: https://docs.airbyte.com/integrations/sources/gitlab
githubIssueLabel: source-gitlab
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
"type": ["null", "string"],
"format": "date-time"
},
"extended_trailers": {
"type": ["null", "object"],
"properties" : {
"Cc": {
"type" : ["null", "array"],
"items" : {
"type" : ["null", "string"]
}
}
}
},
"committer_name": {
"type": ["null", "string"]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@
"emails_disabled": {
"type": ["null", "boolean"]
},
"emails_enabled": {
"type": ["null", "boolean"]
},
"mentions_disabled": {
"type": ["null", "boolean"]
},
Expand Down Expand Up @@ -144,6 +147,9 @@
},
"shared_runners_setting": {
"type": ["null", "string"]
},
"service_access_tokens_expiration_enforced": {
"type": ["null", "boolean"]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -235,20 +235,23 @@
}
},
"epic": {
"id": {
"type": ["null", "integer"]
},
"iid": {
"type": ["null", "integer"]
},
"title": {
"type": ["null", "string"]
},
"url": {
"type": ["null", "string"]
},
"group_id": {
"type": ["null", "integer"]
"type": ["null", "object"],
"properties": {
"id": {
"type": ["null", "integer"]
},
"iid": {
"type": ["null", "integer"]
},
"title": {
"type": ["null", "string"]
},
"url": {
"type": ["null", "string"]
},
"group_id": {
"type": ["null", "integer"]
}
}
},
"epic_iid": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,15 @@
},
"merge_trains_skip_train_allowed": {
"type": ["null", "boolean"]
},
"code_suggestions": {
"type": ["null", "boolean"]
},
"model_registry_access_level": {
"type": ["null", "string"]
},
"ci_restrict_pipeline_cancellation_role": {
"type": ["null", "string"]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapp
elif isinstance(response_data, dict):
yield self.transform(response_data, **kwargs)
else:
Exception(f"Unsupported type of response data for stream {self.name}")
self.logger.info(f"Unsupported type of response data for stream {self.name}")

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

@staticmethod
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"groups": "a b c", "groups_list": ["a", "c", "b"]}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import os
from source_gitlab.config_migrations import MigrateGroups
from source_gitlab.source import SourceGitlab

TEST_CONFIG_PATH = f"{os.path.dirname(__file__)}/test_config.json"


def test_should_migrate():
assert MigrateGroups._should_migrate({"groups": "group group2 group3"}) is True
assert MigrateGroups._should_migrate({"groups_list": ["test", "group2", "group3"]}) is False


def test__modify_and_save():
source = SourceGitlab()
expected = {"groups": "a b c", "groups_list": ["b", "c", "a"]}
modified_config = MigrateGroups._modify_and_save(config_path=TEST_CONFIG_PATH, source=source, config={"groups": "a b c"})
assert modified_config["groups_list"].sort() == expected["groups_list"].sort()
assert modified_config.get("groups")
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,39 @@ def test_connection_fail_due_to_api_error(errror_code, expected_status, config,
assert msg.startswith("Unable to connect to Gitlab API with the provided Private Access Token")


def test_connection_fail_due_to_api_error_oauth(oauth_config, mocker, requests_mock):
mocker.patch("time.sleep")
test_response = {
"access_token": "new_access_token",
"expires_in": 7200,
"created_at": 1735689600,
# (7200 + 1735689600).timestamp().to_rfc3339_string() = "2025-01-01T02:00:00+00:00"
"refresh_token": "new_refresh_token",
}
requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json=test_response)
requests_mock.get("/api/v4/groups", status_code=500)
source = SourceGitlab()
status, msg = source.check_connection(logging.getLogger(), oauth_config)
assert status is False
assert msg.startswith("Unable to connect to Gitlab API with the provided credentials")


def test_connection_fail_due_to_expired_access_token_error(oauth_config, requests_mock):
expected = "Unable to refresh the `access_token`, please re-auth in Source > Settings."
expected = "Unable to refresh the `access_token`, please re-authenticate in Sources > Settings."
requests_mock.post("https://gitlab.com/oauth/token", status_code=401)
source = SourceGitlab()
status, msg = source.check_connection(logging.getLogger("airbyte"), oauth_config)
assert status is False, expected in msg
assert status is False
assert expected in msg


def test_connection_refresh_access_token(oauth_config, requests_mock):
expected = "Unknown error occurred while checking the connection"
requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json={"access_token": "new access token"})
source = SourceGitlab()
status, msg = source.check_connection(logging.getLogger("airbyte"), oauth_config)
assert status is False
assert expected in msg


def test_refresh_expired_access_token_on_error(oauth_config, requests_mock):
Expand All @@ -80,7 +107,7 @@ def test_refresh_expired_access_token_on_error(oauth_config, requests_mock):
# (7200 + 1735689600).timestamp().to_rfc3339_string() = "2025-01-01T02:00:00+00:00"
"refresh_token": "new_refresh_token",
}
expected_token_expiry_date = "2025-01-01T02:00:00+00:00"
expected_token_expiry_date = "2025-01-01 02:00:00+00:00"
requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json=test_response)
requests_mock.get("https://gitlab.com/api/v4/groups?per_page=50", status_code=200, json=[])
source = SourceGitlab()
Expand Down Expand Up @@ -108,3 +135,27 @@ def test_connection_fail_due_to_config_error(mocker, api_url, deployment_env, ex
}
status, msg = source.check_connection(logging.getLogger(), config)
assert (status, msg) == (False, expected_message)


def test_try_refresh_access_token(oauth_config, requests_mock):
test_response = {
"access_token": "new_access_token",
"expires_in": 7200,
"created_at": 1735689600,
# (7200 + 1735689600).timestamp().to_rfc3339_string() = "2025-01-01T02:00:00+00:00"
"refresh_token": "new_refresh_token",
}
requests_mock.post("https://gitlab.com/oauth/token", status_code=200, json=test_response)

expected = {"api_url": "gitlab.com",
"credentials": {"access_token": "new_access_token",
"auth_type": "oauth2.0",
"client_id": "client_id",
"client_secret": "client_secret",
"refresh_token": "new_refresh_token",
"token_expiry_date": "2025-01-01T02:00:00+00:00"},
"start_date": "2021-01-01T00:00:00Z"}

source = SourceGitlab()
source._auth_params(oauth_config)
assert source._try_refresh_access_token(logger=logging.getLogger(), config=oauth_config) == expected
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#

import datetime
from unittest.mock import MagicMock

import pytest
from airbyte_cdk.sources.streams.http.auth import NoAuth
Expand All @@ -18,6 +19,7 @@
Releases,
Tags,
)
from airbyte_cdk.models import SyncMode

auth_params = {"authenticator": NoAuth(), "api_url": "gitlab.com"}
start_date = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=14)
Expand Down Expand Up @@ -276,3 +278,53 @@ def test_transform(requests_mock, stream, response_mocks, expected_records, requ
def test_updated_state(stream, current_state, latest_record, new_state, request):
stream = request.getfixturevalue(stream)
assert stream.get_updated_state(current_state, latest_record) == new_state


def test_parse_response_unsuported_response_type(request, caplog):
stream = request.getfixturevalue("pipelines")
from unittest.mock import MagicMock
response = MagicMock()
response.status_code = 200
response.json = MagicMock(return_value="")
list(stream.parse_response(response=response))
assert "Unsupported type of response data for stream pipelines" in caplog.text


def test_stream_slices_child_stream(request, requests_mock):
commits = request.getfixturevalue("commits")
requests_mock.get("https://gitlab.com/api/v4/projects/p_1?per_page=50&statistics=1",
json=[{"id": 13082000, "description": "", "name": "New CI Test Project"}])

slices = list(commits.stream_slices(sync_mode=SyncMode.full_refresh, stream_state={"13082000": {""'created_at': "2021-03-10T23:58:1213"}}))
assert slices


def test_next_page_token(request):
response = MagicMock()
response.status_code = 200
response.json = MagicMock(return_value=["some data"])
commits = request.getfixturevalue("commits")
assert not commits.next_page_token(response)
data = ["some data" for x in range(0, 50)]
response.json = MagicMock(return_value=data)
assert commits.next_page_token(response) == {'page': 2}
response.json = MagicMock(return_value={"data": "some data"})
assert not commits.next_page_token(response)


def test_availability_strategy(request):
commits = request.getfixturevalue("commits")
assert not commits.availability_strategy


def test_request_params(request):
commits = request.getfixturevalue("commits")
expected = {'per_page': 50, 'page': 2, 'with_stats': True}
assert commits.request_params(stream_slice={"updated_after": "2021-03-10T23:58:1213"}, next_page_token={'page': 2}) == expected


def test_chunk_date_range(request):
commits = request.getfixturevalue("commits")
# start point in future
start_point = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=1)
assert not list(commits._chunk_date_range(start_point))
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from source_gitlab.utils import parse_url
import pytest


@pytest.mark.parametrize(
"url, expected",
(
("http://example.com", (True, "http", "example.com")),
("http://example", (True, "http", "example")),
("test://example.com", (False, "", "")),
("https://example.com/test/test2", (False, "", "")),
)
)
def test_parse_url(url, expected):
assert parse_url(url) == expected
Loading
Loading