diff --git a/setup.cfg b/setup.cfg
index d51fba95..1cc50626 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -71,7 +71,6 @@ tests =
invenio-db[postgresql,mysql]>=2.0.0,<3.0.0
invenio-files-rest>=3.0.0,<4.0.0
isort>=4.2.2
- mock>=2.0.0
pytest-black-ng>=0.4.0
pytest-invenio>=3.0.0,<4.0.0
pytest-mock>=2.0.0
diff --git a/tests/conftest.py b/tests/conftest.py
index e2be18a5..1129eb45 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,55 +1,56 @@
# -*- coding: utf-8 -*-
#
-# This file is part of Invenio.
# Copyright (C) 2023-2025 CERN.
-# Copyright (C) 2025 Graz University of Technology.
#
-# Invenio is free software; you can redistribute it
-# and/or modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation; either version 2 of the
-# License, or (at your option) any later version.
-#
-# Invenio is distributed in the hope that it will be
-# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Invenio; if not, write to the
-# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
-# MA 02111-1307, USA.
-#
-# In applying this license, CERN does not
-# waive the privileges and immunities granted to it by virtue of its status
-# as an Intergovernmental Organization or submit itself to any jurisdiction.
-
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
"""Pytest configuration."""
from __future__ import absolute_import, print_function
from collections import namedtuple
+from typing import Any
-import github3
import pytest
from invenio_app.factory import create_api
-from invenio_oauthclient.contrib.github import REMOTE_APP as GITHUB_REMOTE_APP
-from invenio_oauthclient.contrib.github import REMOTE_REST_APP as GITHUB_REMOTE_REST_APP
from invenio_oauthclient.models import RemoteToken
from invenio_oauthclient.proxies import current_oauthclient
-from mock import MagicMock, patch
+
+from invenio_vcs.contrib.github import GitHubProviderFactory
+from invenio_vcs.contrib.gitlab import GitLabProviderFactory
+from invenio_vcs.generic_models import (
+ GenericContributor,
+ GenericOwner,
+ GenericOwnerType,
+ GenericRelease,
+ GenericRepository,
+ GenericUser,
+ GenericWebhook,
+)
+from invenio_vcs.providers import RepositoryServiceProvider
+from invenio_vcs.service import VCSService
+from invenio_vcs.utils import utcnow
+from tests.contrib_fixtures.github import GitHubPatcher
+from tests.contrib_fixtures.gitlab import GitLabPatcher
+from tests.contrib_fixtures.patcher import TestProviderPatcher
from .fixtures import (
- ZIPBALL,
- TestGithubRelease,
- github_file_contents,
- github_repo_metadata,
- github_user_metadata,
+ TestVCSRelease,
)
@pytest.fixture(scope="module")
def app_config(app_config):
"""Test app config."""
+ vcs_github = GitHubProviderFactory(
+ base_url="https://github.com",
+ webhook_receiver_url="http://localhost:5000/api/receivers/github/events/?access_token={token}",
+ )
+ vcs_gitlab = GitLabProviderFactory(
+ base_url="https://gitlab.com",
+ webhook_receiver_url="http://localhost:5000/api/receivers/gitlab/events/?access_token={token}",
+ )
+
app_config.update(
# HTTPretty doesn't play well with Redis.
# See gabrielfalcao/HTTPretty#110
@@ -63,21 +64,24 @@ def app_config(app_config):
consumer_key="changeme",
consumer_secret="changeme",
),
- GITHUB_SHARED_SECRET="changeme",
- GITHUB_INSECURE_SSL=False,
- GITHUB_INTEGRATION_ENABLED=True,
- GITHUB_METADATA_FILE=".invenio.json",
- GITHUB_WEBHOOK_RECEIVER_URL="http://localhost:5000/api/receivers/github/events/?access_token={token}",
- GITHUB_WEBHOOK_RECEIVER_ID="github",
- GITHUB_RELEASE_CLASS=TestGithubRelease,
+ GITLAB_APP_CREDENTIALS=dict(
+ consumer_key="changeme",
+ consumer_secret="changeme",
+ ),
+ VCS_RELEASE_CLASS=TestVCSRelease,
+ VCS_PROVIDERS=[vcs_github, vcs_gitlab],
+ # TODO: delete this to avoid duplication
+ VCS_INTEGRATION_ENABLED=True,
LOGIN_DISABLED=False,
OAUTHLIB_INSECURE_TRANSPORT=True,
OAUTH2_CACHE_TYPE="simple",
OAUTHCLIENT_REMOTE_APPS=dict(
- github=GITHUB_REMOTE_APP,
+ github=vcs_github.remote_config,
+ gitlab=vcs_gitlab.remote_config,
),
OAUTHCLIENT_REST_REMOTE_APPS=dict(
- github=GITHUB_REMOTE_REST_APP,
+ github=vcs_github.remote_config,
+ gitlab=vcs_gitlab.remote_config,
),
SECRET_KEY="test_key",
SERVER_NAME="testserver.localdomain",
@@ -98,9 +102,6 @@ def app_config(app_config):
FILES_REST_DEFAULT_STORAGE_CLASS="L",
THEME_FRONTPAGE=False,
)
- app_config["OAUTHCLIENT_REMOTE_APPS"]["github"]["params"]["request_token_params"][
- "scope"
- ] = "user:email,admin:repo_hook,read:org"
return app_config
@@ -131,10 +132,10 @@ def running_app(app, location, cache):
@pytest.fixture()
-def test_user(app, db, github_remote_app):
+def test_user(app, db, remote_apps):
"""Creates a test user.
- Links the user to a github RemoteToken.
+ Links the user to a VCS RemoteToken.
"""
datastore = app.extensions["security"].datastore
user = datastore.create_user(
@@ -142,33 +143,29 @@ def test_user(app, db, github_remote_app):
password="tester",
)
- # Create GitHub link for user
- token = RemoteToken.get(user.id, github_remote_app.consumer_key)
- if not token:
- RemoteToken.create(
- user.id,
- github_remote_app.consumer_key,
- "test",
- "",
- )
+ # Create provider links for user
+ for app in remote_apps:
+ token = RemoteToken.get(user.id, app.consumer_key)
+ if not token:
+ # This auto-creates the missing RemoteAccount
+ RemoteToken.create(
+ user.id,
+ app.consumer_key,
+ "test",
+ "",
+ )
+
db.session.commit()
return user
@pytest.fixture()
-def github_remote_app():
- """Returns github remote app."""
- return current_oauthclient.oauth.remote_apps["github"]
-
-
-@pytest.fixture()
-def remote_token(test_user, github_remote_app):
- """Returns github RemoteToken for user."""
- token = RemoteToken.get(
- test_user.id,
- github_remote_app.consumer_key,
- )
- return token
+def remote_apps():
+ """An example list of configured OAuth apps."""
+ return [
+ current_oauthclient.oauth.remote_apps["github"],
+ current_oauthclient.oauth.remote_apps["gitlab"],
+ ]
@pytest.fixture
@@ -190,121 +187,153 @@ def tester_id(test_user):
@pytest.fixture()
-def test_repo_data_one():
- """Test repository."""
- return {"name": "repo-1", "id": 1}
+def test_generic_repositories():
+ """Provider-common dataset of test repositories."""
+ return [
+ GenericRepository(
+ id="1",
+ full_name="repo-1",
+ default_branch="main",
+ description="Lorem ipsum",
+ license_spdx="MIT",
+ ),
+ GenericRepository(
+ id="2",
+ full_name="repo-2",
+ default_branch="main",
+ description="Lorem ipsum",
+ license_spdx="MIT",
+ ),
+ GenericRepository(
+ id="3",
+ full_name="repo-3",
+ default_branch="main",
+ description="Lorem ipsum",
+ license_spdx="MIT",
+ ),
+ ]
+
+
+@pytest.fixture()
+def test_generic_contributors():
+ """Provider-common dataset of test contributors (same for all repos)."""
+ return [
+ GenericContributor(
+ id="1", username="user1", company="Lorem", display_name="Lorem"
+ ),
+ GenericContributor(
+ id="2", username="user2", contributions_count=10, display_name="Lorem"
+ ),
+ ]
+
+
+@pytest.fixture()
+def test_collaborators():
+ """Provider-common dataset of test collaborators (same for all repos).
+
+ We don't have a built-in generic type for this so we'll use a dictionary.
+ """
+ return [
+ {"id": "1", "username": "user1", "admin": True},
+ {"id": "2", "username": "user2", "admin": False},
+ ]
@pytest.fixture()
-def test_repo_data_two():
- """Test repository."""
- return {"name": "repo-2", "id": 2}
+def test_generic_webhooks():
+ """Provider-common dataset of test webhooks (same for all repos)."""
+ return [
+ GenericWebhook(id="1", repository_id="1", url="https://example.com"),
+ GenericWebhook(id="2", repository_id="2", url="https://example.com"),
+ ]
@pytest.fixture()
-def test_repo_data_three():
- """Test repository."""
- return {"name": "arepo", "id": 3}
+def test_generic_user():
+ """Provider-common user to own the repositories."""
+ return GenericUser(id="1", username="user1", display_name="Test User")
@pytest.fixture()
-def github_api(
- running_app,
- db,
- test_repo_data_one,
- test_repo_data_two,
- test_repo_data_three,
- test_user,
-):
- """Github API mock."""
- mock_api = MagicMock()
- mock_api.session = MagicMock()
- mock_api.me.return_value = github3.users.User(
- github_user_metadata(login="auser", email="auser@inveniosoftware.org"),
- mock_api.session,
+def test_generic_owner(test_generic_user: GenericUser):
+ """GenericOwner representation of the test generic user."""
+ return GenericOwner(
+ test_generic_user.id,
+ test_generic_user.username,
+ GenericOwnerType.Person,
+ display_name=test_generic_user.display_name,
)
- repo_1 = github3.repos.Repository(
- github_repo_metadata(
- "auser", test_repo_data_one["name"], test_repo_data_one["id"]
- ),
- mock_api.session,
- )
- repo_1.hooks = MagicMock(return_value=[])
- repo_1.file_contents = MagicMock(return_value=None)
- # Mock hook creation to retun the hook id '12345'
- hook_instance = MagicMock()
- hook_instance.id = 12345
- repo_1.create_hook = MagicMock(return_value=hook_instance)
-
- repo_2 = github3.repos.Repository(
- github_repo_metadata(
- "auser", test_repo_data_two["name"], test_repo_data_two["id"]
- ),
- mock_api.session,
+
+@pytest.fixture()
+def test_generic_release():
+ """Provider-common example release."""
+ return GenericRelease(
+ id="1",
+ tag_name="v1.0",
+ created_at=utcnow(),
+ name="Example release",
+ body="Lorem ipsum dolor sit amet",
+ published_at=utcnow(),
+ tarball_url="https://example.com/v1.0.tar",
+ zipball_url="https://example.com/v1.0.zip",
)
- repo_2.hooks = MagicMock(return_value=[])
- repo_2.create_hook = MagicMock(return_value=hook_instance)
- file_path = "test.py"
+@pytest.fixture()
+def test_file():
+ """Provider-common example file within a repository (no generic interface available for this)."""
+ return {"path": "test.py", "content": "test"}
- def mock_file_content():
- # File contents mocking
- owner = "auser"
- repo = test_repo_data_two["name"]
- ref = ""
- # Dummy data to be encoded as the file contents
- data = "dummy".encode("ascii")
- return github_file_contents(owner, repo, file_path, ref, data)
+_provider_patchers: list[type[TestProviderPatcher]] = [GitHubPatcher, GitLabPatcher]
- file_data = mock_file_content()
- def mock_file_contents(path, ref=None):
- if path == file_path:
- # Mock github3.contents.Content with file_data
- return MagicMock(decoded=file_data)
- return None
+def provider_id(p: type[TestProviderPatcher]):
+ """Extract the provider ID to use as the test case ID."""
+ return p.provider_factory().id
- repo_2.file_contents = MagicMock(side_effect=mock_file_contents)
- repo_3 = github3.repos.Repository(
- github_repo_metadata(
- "auser", test_repo_data_three["name"], test_repo_data_three["id"]
- ),
- mock_api.session,
+@pytest.fixture(params=_provider_patchers, ids=provider_id)
+def vcs_provider(
+ request: pytest.FixtureRequest,
+ test_user,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_contributors: list[GenericContributor],
+ test_collaborators: list[dict[str, Any]],
+ test_generic_webhooks: list[GenericWebhook],
+ test_generic_user: GenericUser,
+ test_file: dict[str, Any],
+):
+ """Call the patcher for the provider and run the test case 'inside' its patch context."""
+ patcher_class: type[TestProviderPatcher] = request.param
+ patcher = patcher_class(test_user)
+ # The patch call returns a generator that yields the provider within the patch context.
+ # Use yield from to delegate to the patcher's generator, ensuring tests run within the patch context.
+ yield from patcher.patch(
+ test_generic_repositories,
+ test_generic_contributors,
+ test_collaborators,
+ test_generic_webhooks,
+ test_generic_user,
+ test_file,
+ )
+
+
+@pytest.fixture()
+def vcs_service(vcs_provider: RepositoryServiceProvider):
+ """Return an initialised (but not synced) service object for a provider."""
+ svc = VCSService(vcs_provider)
+ svc.init_account()
+ return svc
+
+
+@pytest.fixture()
+def provider_patcher(vcs_provider: RepositoryServiceProvider):
+ """Return the raw patcher object corresponding to the current test's provider."""
+ for patcher in _provider_patchers:
+ if patcher.provider_factory().id == vcs_provider.factory.id:
+ return patcher
+ raise ValueError(
+ f"Patcher corresponding to ID {vcs_provider.factory.id} not found."
)
- repo_3.hooks = MagicMock(return_value=[])
- repo_3.file_contents = MagicMock(return_value=None)
-
- repos = {1: repo_1, 2: repo_2, 3: repo_3}
- repos_by_name = {r.full_name: r for r in repos.values()}
- mock_api.repositories.return_value = repos.values()
-
- def mock_repo_with_id(id):
- return repos.get(id)
-
- def mock_repo_by_name(owner, name):
- return repos_by_name.get("/".join((owner, name)))
-
- def mock_head_status_by_repo_url(url, **kwargs):
- url_specific_refs_tags = (
- "https://github.com/auser/repo-2/zipball/refs/tags/v1.0-tag-and-branch"
- )
- if url.endswith("v1.0-tag-and-branch") and url != url_specific_refs_tags:
- return MagicMock(
- status_code=300, links={"alternate": {"url": url_specific_refs_tags}}
- )
- else:
- return MagicMock(status_code=200, url=url)
-
- mock_api.repository_with_id.side_effect = mock_repo_with_id
- mock_api.repository.side_effect = mock_repo_by_name
- mock_api.markdown.side_effect = lambda x: x
- mock_api.session.head.side_effect = mock_head_status_by_repo_url
- mock_api.session.get.return_value = MagicMock(raw=ZIPBALL())
-
- with patch("invenio_github.api.GitHubAPI.api", new=mock_api):
- with patch("invenio_github.api.GitHubAPI._sync_hooks"):
- yield mock_api
diff --git a/tests/contrib_fixtures/github.py b/tests/contrib_fixtures/github.py
new file mode 100644
index 00000000..c30c8215
--- /dev/null
+++ b/tests/contrib_fixtures/github.py
@@ -0,0 +1,813 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2025 CERN.
+#
+# Invenio-Github is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+"""Fixture test impl for GitHub."""
+
+import os
+from base64 import b64encode
+from io import BytesIO
+from typing import Any, Iterator
+from unittest.mock import MagicMock, patch
+from zipfile import ZipFile
+
+import github3
+import github3.repos
+import github3.repos.hook
+import github3.users
+
+from invenio_vcs.contrib.github import GitHubProviderFactory
+from invenio_vcs.generic_models import (
+ GenericContributor,
+ GenericOwner,
+ GenericRelease,
+ GenericRepository,
+ GenericUser,
+ GenericWebhook,
+)
+from invenio_vcs.providers import (
+ RepositoryServiceProvider,
+ RepositoryServiceProviderFactory,
+)
+from tests.contrib_fixtures.patcher import TestProviderPatcher
+
+
+def github_user_metadata(
+ id: int, display_name: str | None, login, email=None, bio=True
+):
+ """Github user fixture generator."""
+ username = login
+
+ user = {
+ "avatar_url": "https://avatars.githubusercontent.com/u/7533764?",
+ "collaborators": 0,
+ "created_at": "2014-05-09T12:26:44Z",
+ "disk_usage": 0,
+ "events_url": "https://api.github.com/users/%s/events{/privacy}" % username,
+ "followers": 0,
+ "followers_url": "https://api.github.com/users/%s/followers" % username,
+ "following": 0,
+ "following_url": "https://api.github.com/users/%s/"
+ "following{/other_user}" % username,
+ "gists_url": "https://api.github.com/users/%s/gists{/gist_id}" % username,
+ "gravatar_id": "12345678",
+ "html_url": "https://github.com/%s" % username,
+ "id": id,
+ "login": "%s" % username,
+ "organizations_url": "https://api.github.com/users/%s/orgs" % username,
+ "owned_private_repos": 0,
+ "plan": {
+ "collaborators": 0,
+ "name": "free",
+ "private_repos": 0,
+ "space": 307200,
+ },
+ "private_gists": 0,
+ "public_gists": 0,
+ "public_repos": 0,
+ "received_events_url": "https://api.github.com/users/%s/"
+ "received_events" % username,
+ "repos_url": "https://api.github.com/users/%s/repos" % username,
+ "site_admin": False,
+ "starred_url": "https://api.github.com/users/%s/"
+ "starred{/owner}{/repo}" % username,
+ "subscriptions_url": "https://api.github.com/users/%s/"
+ "subscriptions" % username,
+ "total_private_repos": 0,
+ "type": "User",
+ "updated_at": "2014-05-09T12:26:44Z",
+ "url": "https://api.github.com/users/%s" % username,
+ "hireable": False,
+ "location": "Geneve",
+ }
+
+ if bio:
+ user.update(
+ {
+ "bio": "Software Engineer at CERN",
+ "blog": "http://www.cern.ch",
+ "company": "CERN",
+ "name": display_name,
+ }
+ )
+
+ if email is not None:
+ user.update(
+ {
+ "email": email,
+ }
+ )
+
+ return user
+
+
+def github_repo_metadata(
+ owner_username: str,
+ owner_id: int,
+ repo_name: str,
+ repo_id: int,
+ default_branch: str,
+):
+ """Github repository fixture generator."""
+ repo_url = "%s/%s" % (owner_username, repo_name)
+
+ return {
+ "archive_url": "https://api.github.com/repos/%s/"
+ "{archive_format}{/ref}" % repo_url,
+ "assignees_url": "https://api.github.com/repos/%s/"
+ "assignees{/user}" % repo_url,
+ "blobs_url": "https://api.github.com/repos/%s/git/blobs{/sha}" % repo_url,
+ "branches_url": "https://api.github.com/repos/%s/"
+ "branches{/branch}" % repo_url,
+ "clone_url": "https://github.com/%s.git" % repo_url,
+ "collaborators_url": "https://api.github.com/repos/%s/"
+ "collaborators{/collaborator}" % repo_url,
+ "comments_url": "https://api.github.com/repos/%s/"
+ "comments{/number}" % repo_url,
+ "commits_url": "https://api.github.com/repos/%s/commits{/sha}" % repo_url,
+ "compare_url": "https://api.github.com/repos/%s/compare/"
+ "{base}...{head}" % repo_url,
+ "contents_url": "https://api.github.com/repos/%s/contents/{+path}" % repo_url,
+ "contributors_url": "https://api.github.com/repos/%s/contributors" % repo_url,
+ "created_at": "2012-10-29T10:24:02Z",
+ "default_branch": default_branch,
+ "description": "",
+ "downloads_url": "https://api.github.com/repos/%s/downloads" % repo_url,
+ "events_url": "https://api.github.com/repos/%s/events" % repo_url,
+ "fork": False,
+ "forks": 0,
+ "forks_count": 0,
+ "forks_url": "https://api.github.com/repos/%s/forks" % repo_url,
+ "full_name": repo_url,
+ "git_commits_url": "https://api.github.com/repos/%s/git/"
+ "commits{/sha}" % repo_url,
+ "git_refs_url": "https://api.github.com/repos/%s/git/refs{/sha}" % repo_url,
+ "git_tags_url": "https://api.github.com/repos/%s/git/tags{/sha}" % repo_url,
+ "git_url": "git://github.com/%s.git" % repo_url,
+ "has_downloads": True,
+ "has_issues": True,
+ "has_wiki": True,
+ "homepage": None,
+ "hooks_url": "https://api.github.com/repos/%s/hooks" % repo_url,
+ "html_url": "https://github.com/%s" % repo_url,
+ "id": repo_id,
+ "issue_comment_url": "https://api.github.com/repos/%s/issues/"
+ "comments/{number}" % repo_url,
+ "issue_events_url": "https://api.github.com/repos/%s/issues/"
+ "events{/number}" % repo_url,
+ "issues_url": "https://api.github.com/repos/%s/issues{/number}" % repo_url,
+ "keys_url": "https://api.github.com/repos/%s/keys{/key_id}" % repo_url,
+ "labels_url": "https://api.github.com/repos/%s/labels{/name}" % repo_url,
+ "language": None,
+ "languages_url": "https://api.github.com/repos/%s/languages" % repo_url,
+ "merges_url": "https://api.github.com/repos/%s/merges" % repo_url,
+ "milestones_url": "https://api.github.com/repos/%s/"
+ "milestones{/number}" % repo_url,
+ "mirror_url": None,
+ "name": "altantis-conf",
+ "notifications_url": "https://api.github.com/repos/%s/"
+ "notifications{?since,all,participating}",
+ "open_issues": 0,
+ "open_issues_count": 0,
+ "owner": {
+ "avatar_url": "https://avatars.githubusercontent.com/u/1234?",
+ "events_url": "https://api.github.com/users/%s/"
+ "events{/privacy}" % owner_username,
+ "followers_url": "https://api.github.com/users/%s/followers"
+ % owner_username,
+ "following_url": "https://api.github.com/users/%s/"
+ "following{/other_user}" % owner_username,
+ "gists_url": "https://api.github.com/users/%s/gists{/gist_id}"
+ % owner_username,
+ "gravatar_id": "1234",
+ "html_url": "https://github.com/%s" % owner_username,
+ "id": owner_id,
+ "login": "%s" % owner_username,
+ "organizations_url": "https://api.github.com/users/%s/orgs"
+ % owner_username,
+ "received_events_url": "https://api.github.com/users/%s/"
+ "received_events" % owner_username,
+ "repos_url": "https://api.github.com/users/%s/repos" % owner_username,
+ "site_admin": False,
+ "starred_url": "https://api.github.com/users/%s/"
+ "starred{/owner}{/repo}" % owner_username,
+ "subscriptions_url": "https://api.github.com/users/%s/"
+ "subscriptions" % owner_username,
+ "type": "User",
+ "url": "https://api.github.com/users/%s" % owner_username,
+ },
+ "permissions": {"admin": True, "pull": True, "push": True},
+ "private": False,
+ "pulls_url": "https://api.github.com/repos/%s/pulls{/number}" % repo_url,
+ "pushed_at": "2012-10-29T10:28:08Z",
+ "releases_url": "https://api.github.com/repos/%s/releases{/id}" % repo_url,
+ "size": 104,
+ "ssh_url": "git@github.com:%s.git" % repo_url,
+ "stargazers_count": 0,
+ "stargazers_url": "https://api.github.com/repos/%s/stargazers" % repo_url,
+ "statuses_url": "https://api.github.com/repos/%s/statuses/{sha}" % repo_url,
+ "subscribers_url": "https://api.github.com/repos/%s/subscribers" % repo_url,
+ "subscription_url": "https://api.github.com/repos/%s/subscription" % repo_url,
+ "svn_url": "https://github.com/%s" % repo_url,
+ "tags_url": "https://api.github.com/repos/%s/tags" % repo_url,
+ "teams_url": "https://api.github.com/repos/%s/teams" % repo_url,
+ "trees_url": "https://api.github.com/repos/%s/git/trees{/sha}" % repo_url,
+ "updated_at": "2013-10-25T11:30:04Z",
+ "url": "https://api.github.com/repos/%s" % repo_url,
+ "watchers": 0,
+ "watchers_count": 0,
+ "deployments_url": "https://api.github.com/repos/%s/deployments" % repo_url,
+ "archived": False,
+ "has_pages": False,
+ "has_projects": False,
+ "network_count": 0,
+ "subscribers_count": 0,
+ }
+
+
+def github_zipball():
+ """Github repository ZIP fixture."""
+ memfile = BytesIO()
+ zipfile = ZipFile(memfile, "w")
+ zipfile.writestr("test.txt", "hello world")
+ zipfile.close()
+ memfile.seek(0)
+ return memfile
+
+
+def github_webhook_payload(
+ sender, repo, repo_id, default_branch: str, tag: str = "v1.0"
+):
+ """Github payload fixture generator."""
+ c = dict(repo=repo, user=sender, url="%s/%s" % (sender, repo), id="4321", tag=tag)
+
+ return {
+ "action": "published",
+ "release": {
+ "url": "https://api.github.com/repos/%(url)s/releases/%(id)s" % c,
+ "assets_url": "https://api.github.com/repos/%(url)s/releases/"
+ "%(id)s/assets" % c,
+ "upload_url": "https://uploads.github.com/repos/%(url)s/"
+ "releases/%(id)s/assets{?name}" % c,
+ "html_url": "https://github.com/%(url)s/releases/tag/%(tag)s" % c,
+ "id": int(c["id"]),
+ "tag_name": c["tag"],
+ "target_commitish": default_branch,
+ "name": "Release name",
+ "body": "",
+ "draft": False,
+ "author": {
+ "login": "%(user)s" % c,
+ "id": 1698163,
+ "avatar_url": "https://avatars.githubusercontent.com/u/12345",
+ "gravatar_id": "12345678",
+ "url": "https://api.github.com/users/%(user)s" % c,
+ "html_url": "https://github.com/%(user)s" % c,
+ "followers_url": "https://api.github.com/users/%(user)s/"
+ "followers" % c,
+ "following_url": "https://api.github.com/users/%(user)s/"
+ "following{/other_user}" % c,
+ "gists_url": "https://api.github.com/users/%(user)s/"
+ "gists{/gist_id}" % c,
+ "starred_url": "https://api.github.com/users/%(user)s/"
+ "starred{/owner}{/repo}" % c,
+ "subscriptions_url": "https://api.github.com/users/%(user)s/"
+ "subscriptions" % c,
+ "organizations_url": "https://api.github.com/users/%(user)s/"
+ "orgs" % c,
+ "repos_url": "https://api.github.com/users/%(user)s/repos" % c,
+ "events_url": "https://api.github.com/users/%(user)s/"
+ "events{/privacy}" % c,
+ "received_events_url": "https://api.github.com/users/"
+ "%(user)s/received_events" % c,
+ "type": "User",
+ "site_admin": False,
+ },
+ "prerelease": False,
+ "created_at": "2014-02-26T08:13:42Z",
+ "published_at": "2014-02-28T13:55:32Z",
+ "assets": [],
+ "tarball_url": "https://api.github.com/repos/%(url)s/"
+ "tarball/%(tag)s" % c,
+ "zipball_url": "https://api.github.com/repos/%(url)s/"
+ "zipball/%(tag)s" % c,
+ },
+ "repository": {
+ "id": repo_id,
+ "name": repo,
+ "full_name": "%(url)s" % c,
+ "owner": {
+ "login": "%(user)s" % c,
+ "id": 1698163,
+ "avatar_url": "https://avatars.githubusercontent.com/u/" "1698163",
+ "gravatar_id": "bbc951080061fc48cae0279d27f3c015",
+ "url": "https://api.github.com/users/%(user)s" % c,
+ "html_url": "https://github.com/%(user)s" % c,
+ "followers_url": "https://api.github.com/users/%(user)s/"
+ "followers" % c,
+ "following_url": "https://api.github.com/users/%(user)s/"
+ "following{/other_user}" % c,
+ "gists_url": "https://api.github.com/users/%(user)s/"
+ "gists{/gist_id}" % c,
+ "starred_url": "https://api.github.com/users/%(user)s/"
+ "starred{/owner}{/repo}" % c,
+ "subscriptions_url": "https://api.github.com/users/%(user)s/"
+ "subscriptions" % c,
+ "organizations_url": "https://api.github.com/users/%(user)s/"
+ "orgs" % c,
+ "repos_url": "https://api.github.com/users/%(user)s/" "repos" % c,
+ "events_url": "https://api.github.com/users/%(user)s/"
+ "events{/privacy}" % c,
+ "received_events_url": "https://api.github.com/users/"
+ "%(user)s/received_events" % c,
+ "type": "User",
+ "site_admin": False,
+ },
+ "private": False,
+ "html_url": "https://github.com/%(url)s" % c,
+ "description": "Repo description.",
+ "fork": True,
+ "url": "https://api.github.com/repos/%(url)s" % c,
+ "forks_url": "https://api.github.com/repos/%(url)s/forks" % c,
+ "keys_url": "https://api.github.com/repos/%(url)s/" "keys{/key_id}" % c,
+ "collaborators_url": "https://api.github.com/repos/%(url)s/"
+ "collaborators{/collaborator}" % c,
+ "teams_url": "https://api.github.com/repos/%(url)s/teams" % c,
+ "hooks_url": "https://api.github.com/repos/%(url)s/hooks" % c,
+ "issue_events_url": "https://api.github.com/repos/%(url)s/"
+ "issues/events{/number}" % c,
+ "events_url": "https://api.github.com/repos/%(url)s/events" % c,
+ "assignees_url": "https://api.github.com/repos/%(url)s/"
+ "assignees{/user}" % c,
+ "branches_url": "https://api.github.com/repos/%(url)s/"
+ "branches{/branch}" % c,
+ "tags_url": "https://api.github.com/repos/%(url)s/tags" % c,
+ "blobs_url": "https://api.github.com/repos/%(url)s/git/" "blobs{/sha}" % c,
+ "git_tags_url": "https://api.github.com/repos/%(url)s/git/"
+ "tags{/sha}" % c,
+ "git_refs_url": "https://api.github.com/repos/%(url)s/git/"
+ "refs{/sha}" % c,
+ "trees_url": "https://api.github.com/repos/%(url)s/git/" "trees{/sha}" % c,
+ "statuses_url": "https://api.github.com/repos/%(url)s/"
+ "statuses/{sha}" % c,
+ "languages_url": "https://api.github.com/repos/%(url)s/" "languages" % c,
+ "stargazers_url": "https://api.github.com/repos/%(url)s/" "stargazers" % c,
+ "contributors_url": "https://api.github.com/repos/%(url)s/"
+ "contributors" % c,
+ "subscribers_url": "https://api.github.com/repos/%(url)s/"
+ "subscribers" % c,
+ "subscription_url": "https://api.github.com/repos/%(url)s/"
+ "subscription" % c,
+ "commits_url": "https://api.github.com/repos/%(url)s/" "commits{/sha}" % c,
+ "git_commits_url": "https://api.github.com/repos/%(url)s/git/"
+ "commits{/sha}" % c,
+ "comments_url": "https://api.github.com/repos/%(url)s/"
+ "comments{/number}" % c,
+ "issue_comment_url": "https://api.github.com/repos/%(url)s/"
+ "issues/comments/{number}" % c,
+ "contents_url": "https://api.github.com/repos/%(url)s/"
+ "contents/{+path}" % c,
+ "compare_url": "https://api.github.com/repos/%(url)s/"
+ "compare/{base}...{head}" % c,
+ "merges_url": "https://api.github.com/repos/%(url)s/merges" % c,
+ "archive_url": "https://api.github.com/repos/%(url)s/"
+ "{archive_format}{/ref}" % c,
+ "downloads_url": "https://api.github.com/repos/%(url)s/" "downloads" % c,
+ "issues_url": "https://api.github.com/repos/%(url)s/" "issues{/number}" % c,
+ "pulls_url": "https://api.github.com/repos/%(url)s/" "pulls{/number}" % c,
+ "milestones_url": "https://api.github.com/repos/%(url)s/"
+ "milestones{/number}" % c,
+ "notifications_url": "https://api.github.com/repos/%(url)s/"
+ "notifications{?since,all,participating}" % c,
+ "labels_url": "https://api.github.com/repos/%(url)s/" "labels{/name}" % c,
+ "releases_url": "https://api.github.com/repos/%(url)s/" "releases{/id}" % c,
+ "created_at": "2014-02-26T07:39:11Z",
+ "updated_at": "2014-02-28T13:55:32Z",
+ "pushed_at": "2014-02-28T13:55:32Z",
+ "git_url": "git://github.com/%(url)s.git" % c,
+ "ssh_url": "git@github.com:%(url)s.git" % c,
+ "clone_url": "https://github.com/%(url)s.git" % c,
+ "svn_url": "https://github.com/%(url)s" % c,
+ "homepage": None,
+ "size": 388,
+ "stargazers_count": 0,
+ "watchers_count": 0,
+ "language": "Python",
+ "has_issues": False,
+ "has_downloads": True,
+ "has_wiki": True,
+ "forks_count": 0,
+ "mirror_url": None,
+ "open_issues_count": 0,
+ "forks": 0,
+ "open_issues": 0,
+ "watchers": 0,
+ "default_branch": default_branch,
+ "master_branch": default_branch,
+ },
+ "sender": {
+ "login": "%(user)s" % c,
+ "id": 1698163,
+ "avatar_url": "https://avatars.githubusercontent.com/u/1234578",
+ "gravatar_id": "12345678",
+ "url": "https://api.github.com/users/%(user)s" % c,
+ "html_url": "https://github.com/%(user)s" % c,
+ "followers_url": "https://api.github.com/users/%(user)s/" "followers" % c,
+ "following_url": "https://api.github.com/users/%(user)s/"
+ "following{/other_user}" % c,
+ "gists_url": "https://api.github.com/users/%(user)s/" "gists{/gist_id}" % c,
+ "starred_url": "https://api.github.com/users/%(user)s/"
+ "starred{/owner}{/repo}" % c,
+ "subscriptions_url": "https://api.github.com/users/%(user)s/"
+ "subscriptions" % c,
+ "organizations_url": "https://api.github.com/users/%(user)s/" "orgs" % c,
+ "repos_url": "https://api.github.com/users/%(user)s/repos" % c,
+ "events_url": "https://api.github.com/users/%(user)s/"
+ "events{/privacy}" % c,
+ "received_events_url": "https://api.github.com/users/%(user)s/"
+ "received_events" % c,
+ "type": "User",
+ "site_admin": False,
+ },
+ }
+
+
+def github_organization_metadata(login):
+ """Github organization fixture generator."""
+ return {
+ "login": login,
+ "id": 1234,
+ "url": "https://api.github.com/orgs/%s" % login,
+ "repos_url": "https://api.github.com/orgs/%s/repos" % login,
+ "events_url": "https://api.github.com/orgs/%s/events" % login,
+ "members_url": "https://api.github.com/orgs/%s/" "members{/member}" % login,
+ "public_members_url": "https://api.github.com/orgs/%s/"
+ "public_members{/member}" % login,
+ "avatar_url": "https://avatars.githubusercontent.com/u/1234?",
+ }
+
+
+def github_collaborator_metadata(admin: bool, login: str, id: int):
+ """Generate metadata for a repo collaborator."""
+ return {
+ "login": login,
+ "id": id,
+ "node_id": "MDQ6VXNlcjE=",
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/%s" % login,
+ "html_url": "https://github.com/%s" % login,
+ "followers_url": "https://api.github.com/users/%s/followers" % login,
+ "following_url": "https://api.github.com/users/%s/following{/other_user}"
+ % login,
+ "gists_url": "https://api.github.com/users/%s/gists{/gist_id}" % login,
+ "starred_url": "https://api.github.com/users/%s/starred{/owner}{/repo}" % login,
+ "subscriptions_url": "https://api.github.com/users/%s/subscriptions" % login,
+ "organizations_url": "https://api.github.com/users/%s/orgs" % login,
+ "repos_url": "https://api.github.com/users/%s/repos" % login,
+ "events_url": "https://api.github.com/users/%s/events{/privacy}" % login,
+ "received_events_url": "https://api.github.com/users/%s/received_events"
+ % login,
+ "type": "User",
+ "site_admin": False,
+ "permissions": {
+ "pull": True,
+ "triage": True,
+ "push": True,
+ "maintain": True,
+ "admin": admin,
+ },
+ "role_name": "write",
+ }
+
+
+def github_contributor_metadata(id: int, login: str, contributions: int):
+ """Generate metadata for a repo contributor."""
+ return {
+ "login": login,
+ "id": id,
+ "node_id": "MDQ6VXNlcjE=",
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/%s" % login,
+ "html_url": "https://github.com/%s" % login,
+ "followers_url": "https://api.github.com/users/%s/followers" % login,
+ "following_url": "https://api.github.com/users/%s/following{/other_user}"
+ % login,
+ "gists_url": "https://api.github.com/users/%s/gists{/gist_id}" % login,
+ "starred_url": "https://api.github.com/users/%s/starred{/owner}{/repo}" % login,
+ "subscriptions_url": "https://api.github.com/users/%s/subscriptions" % login,
+ "organizations_url": "https://api.github.com/users/%s/orgs" % login,
+ "repos_url": "https://api.github.com/users/%s/repos" % login,
+ "events_url": "https://api.github.com/users/%s/events{/privacy}" % login,
+ "received_events_url": "https://api.github.com/users/%s/received_events"
+ % login,
+ "type": "User",
+ "site_admin": False,
+ "contributions": contributions,
+ }
+
+
+def github_webhook_metadata(id: int, url: str, repo_name: str):
+ """Generate metadata for a repo webhook."""
+ return {
+ "type": "Repository",
+ "id": id,
+ "name": "web",
+ "active": True,
+ "events": ["push", "pull_request"],
+ "config": {
+ "content_type": "json",
+ "insecure_ssl": "0",
+ "url": url,
+ },
+ "updated_at": "2019-06-03T00:57:16Z",
+ "created_at": "2019-06-03T00:57:16Z",
+ "url": "https://api.github.com/repos/%s/hooks/%d" % (repo_name, id),
+ "test_url": "https://api.github.com/repos/%s/hooks/%d/test" % (repo_name, id),
+ "ping_url": "https://api.github.com/repos/%s/hooks/%d/pings" % (repo_name, id),
+ "deliveries_url": "https://api.github.com/repos/%s/hooks/%d/deliveries"
+ % (repo_name, id),
+ "last_response": {"code": None, "status": "unused", "message": None},
+ }
+
+
+def github_release_metadata(
+ id: int,
+ repo_name: str,
+ tag_name: str,
+ release_name: str | None,
+ release_description: str | None,
+):
+ """Generate metadata for a release."""
+ return {
+ "url": "https://api.github.com/repos/%s/releases/%d" % (repo_name, id),
+ "html_url": "https://github.com/%s/releases/%s" % (repo_name, tag_name),
+ "assets_url": "https://api.github.com/repos/%s/releases/%d/assets"
+ % (repo_name, id),
+ "upload_url": "https://uploads.github.com/repos/%s/releases/%d/assets{?name,label}"
+ % (repo_name, id),
+ "tarball_url": "https://api.github.com/repos/%s/tarball/%s"
+ % (repo_name, tag_name),
+ "zipball_url": "https://api.github.com/repos/%s/zipball/%s"
+ % (repo_name, tag_name),
+ "id": id,
+ "node_id": "MDc6UmVsZWFzZTE=",
+ "tag_name": tag_name,
+ "target_commitish": "master",
+ "name": release_name,
+ "body": release_description,
+ "draft": False,
+ "prerelease": False,
+ "immutable": True,
+ "created_at": "2013-02-27T19:35:32Z",
+ "published_at": "2013-02-27T19:35:32Z",
+ "author": {
+ "login": "octocat",
+ "id": 1,
+ "node_id": "MDQ6VXNlcjE=",
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/octocat",
+ "html_url": "https://github.com/octocat",
+ "followers_url": "https://api.github.com/users/octocat/followers",
+ "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+ "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+ "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+ "organizations_url": "https://api.github.com/users/octocat/orgs",
+ "repos_url": "https://api.github.com/users/octocat/repos",
+ "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+ "received_events_url": "https://api.github.com/users/octocat/received_events",
+ "type": "User",
+ "site_admin": False,
+ },
+ "assets": [
+ {
+ "url": "https://api.github.com/repos/%s/releases/assets/1" % repo_name,
+ "browser_download_url": "https://github.com/%s/releases/download/%s/example.zip"
+ % (repo_name, tag_name),
+ "id": 1,
+ "node_id": "MDEyOlJlbGVhc2VBc3NldDE=",
+ "name": "example.zip",
+ "label": "short description",
+ "state": "uploaded",
+ "content_type": "application/zip",
+ "size": 1024,
+ "digest": "sha256:2151b604e3429bff440b9fbc03eb3617bc2603cda96c95b9bb05277f9ddba255",
+ "download_count": 42,
+ "created_at": "2013-02-27T19:35:32Z",
+ "updated_at": "2013-02-27T19:35:32Z",
+ "uploader": {
+ "login": "octocat",
+ "id": 1,
+ "node_id": "MDQ6VXNlcjE=",
+ "avatar_url": "https://github.com/images/error/octocat_happy.gif",
+ "gravatar_id": "",
+ "url": "https://api.github.com/users/octocat",
+ "html_url": "https://github.com/octocat",
+ "followers_url": "https://api.github.com/users/octocat/followers",
+ "following_url": "https://api.github.com/users/octocat/following{/other_user}",
+ "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
+ "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
+ "subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
+ "organizations_url": "https://api.github.com/users/octocat/orgs",
+ "repos_url": "https://api.github.com/users/octocat/repos",
+ "events_url": "https://api.github.com/users/octocat/events{/privacy}",
+ "received_events_url": "https://api.github.com/users/octocat/received_events",
+ "type": "User",
+ "site_admin": False,
+ },
+ }
+ ],
+ }
+
+
+def github_webhook_payload(
+ id: int,
+ tag_name: str,
+ release_name: str | None,
+ release_description: str | None,
+ repo_id: int,
+ repo_name: str,
+ repo_owner_id: int,
+ repo_owner_username: str,
+ repo_default_branch: str,
+):
+ """Generate sample payload for a release event webhook."""
+ return {
+ "action": "created",
+ "release": github_release_metadata(
+ id, repo_name, tag_name, release_name, release_description
+ ),
+ "repository": github_repo_metadata(
+ repo_owner_username, repo_owner_id, repo_name, repo_id, repo_default_branch
+ ),
+ }
+
+
+class GitHubPatcher(TestProviderPatcher):
+ """Patch the GitHub API primitives to avoid real API calls and return test data instead."""
+
+ @staticmethod
+ def provider_factory() -> RepositoryServiceProviderFactory:
+ """GitHub provider factory."""
+ return GitHubProviderFactory(
+ base_url="https://github.com",
+ webhook_receiver_url="http://localhost:5000/api/receivers/github/events/?access_token={token}",
+ )
+
+ @staticmethod
+ def test_webhook_payload(
+ generic_repository: GenericRepository,
+ generic_release: GenericRelease,
+ generic_repo_owner: GenericOwner,
+ ) -> dict[str, Any]:
+ """Return a sample webhook payload."""
+ return github_webhook_payload(
+ int(generic_release.id),
+ generic_release.tag_name,
+ generic_release.name,
+ generic_release.body,
+ int(generic_repository.id),
+ generic_repository.full_name,
+ int(generic_repo_owner.id),
+ generic_repo_owner.path_name,
+ generic_repository.default_branch,
+ )
+
+ def patch(
+ self,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_contributors: list[GenericContributor],
+ test_collaborators: list[dict[str, Any]],
+ test_generic_webhooks: list[GenericWebhook],
+ test_generic_user: GenericUser,
+ test_file: dict[str, Any],
+ ) -> Iterator[RepositoryServiceProvider]:
+ """Configure the patch and yield within the patched context."""
+ mock_api = MagicMock()
+ mock_api.session = MagicMock()
+ mock_api.me.return_value = github3.users.User(
+ github_user_metadata(
+ id=int(test_generic_user.id),
+ display_name=test_generic_user.display_name,
+ login=test_generic_user.username,
+ email="%s@inveniosoftware.org" % test_generic_user.username,
+ ),
+ mock_api.session,
+ )
+
+ contributors: list[github3.users.Contributor] = []
+ for generic_contributor in test_generic_contributors:
+ contributor = github3.users.Contributor(
+ github_contributor_metadata(
+ int(generic_contributor.id),
+ generic_contributor.username,
+ generic_contributor.contributions_count or 0,
+ ),
+ mock_api.session,
+ )
+ contributor.refresh = MagicMock(
+ return_value=github3.users.User(
+ github_user_metadata(
+ int(generic_contributor.id),
+ generic_contributor.display_name,
+ generic_contributor.username,
+ "%s@inveniosoftware.org" % generic_contributor.username,
+ ),
+ mock_api.session,
+ )
+ )
+ contributors.append(contributor)
+
+ collaborators: list[github3.users.Collaborator] = []
+ for collaborator in test_collaborators:
+ collaborators.append(
+ github3.users.Collaborator(
+ github_collaborator_metadata(
+ collaborator["admin"],
+ collaborator["username"],
+ int(collaborator["id"]),
+ ),
+ mock_api.session,
+ )
+ )
+
+ repos: dict[int, github3.repos.Repository] = {}
+ for generic_repo in test_generic_repositories:
+ repo = github3.repos.ShortRepository(
+ github_repo_metadata(
+ "auser",
+ 1,
+ generic_repo.full_name,
+ int(generic_repo.id),
+ generic_repo.default_branch,
+ ),
+ mock_api.session,
+ )
+
+ hooks: list[github3.repos.hook.Hook] = []
+ for hook in test_generic_webhooks:
+ if hook.id != generic_repo.id:
+ continue
+
+ hooks.append(
+ github3.repos.hook.Hook(
+ github_webhook_metadata(
+ int(hook.id), hook.url, generic_repo.full_name
+ ),
+ mock_api.session,
+ )
+ )
+
+ repo.hooks = MagicMock(return_value=hooks)
+ repo.file_contents = MagicMock(return_value=None)
+ # Mock hook creation to return the hook id '12345'
+ hook_instance = MagicMock()
+ hook_instance.id = 12345
+ repo.create_hook = MagicMock(return_value=hook_instance)
+ repo.collaborators = MagicMock(return_value=collaborators)
+ repo.contributors = MagicMock(return_value=contributors)
+
+ def mock_file_contents(path: str, ref: str):
+ if path == test_file["path"]:
+ # Mock github3.contents.Content with file data
+ return MagicMock(decoded=test_file["content"].encode("ascii"))
+ raise github3.exceptions.NotFoundError(MagicMock(status_code=404))
+
+ repo.file_contents = MagicMock(side_effect=mock_file_contents)
+
+ repos[int(generic_repo.id)] = repo
+
+ repos_by_name = {r.full_name: r for r in repos.values()}
+ mock_api.repositories.return_value = repos.values()
+
+ def mock_repo_with_id(id):
+ return repos.get(id)
+
+ def mock_repo_by_name(owner, name):
+ return repos_by_name.get("/".join((owner, name)))
+
+ def mock_head_status_by_repo_url(url, **kwargs):
+ url_specific_refs_tags = (
+ "https://github.com/auser/repo-2/zipball/refs/tags/v1.0-tag-and-branch"
+ )
+ if url.endswith("v1.0-tag-and-branch") and url != url_specific_refs_tags:
+ return MagicMock(
+ status_code=300,
+ links={"alternate": {"url": url_specific_refs_tags}},
+ )
+ else:
+ return MagicMock(status_code=200, url=url)
+
+ mock_api.repository_with_id.side_effect = mock_repo_with_id
+ mock_api.repository.side_effect = mock_repo_by_name
+ mock_api.markdown.side_effect = lambda x: x
+ mock_api.session.head.side_effect = mock_head_status_by_repo_url
+ mock_api.session.get.return_value = MagicMock(raw=github_zipball())
+
+ with patch("invenio_vcs.contrib.github.GitHubProvider._gh", new=mock_api):
+ yield self.provider
diff --git a/tests/contrib_fixtures/gitlab.py b/tests/contrib_fixtures/gitlab.py
new file mode 100644
index 00000000..b4282c06
--- /dev/null
+++ b/tests/contrib_fixtures/gitlab.py
@@ -0,0 +1,396 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2025 CERN.
+#
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+#
+# Some of the code in this file was taken from https://codebase.helmholtz.cloud/rodare/invenio-gitlab
+# and relicensed under MIT with permission from the authors.
+"""Fixture test impl for GitLab."""
+
+from typing import Any, Iterator
+from unittest.mock import MagicMock, patch
+
+import gitlab.const
+import gitlab.v4.objects
+
+from invenio_vcs.contrib.gitlab import GitLabProviderFactory
+from invenio_vcs.generic_models import (
+ GenericContributor,
+ GenericOwner,
+ GenericRelease,
+ GenericRepository,
+ GenericUser,
+ GenericWebhook,
+)
+from invenio_vcs.providers import (
+ RepositoryServiceProvider,
+ RepositoryServiceProviderFactory,
+)
+from tests.contrib_fixtures.patcher import TestProviderPatcher
+
+
+def gitlab_namespace_metadata(id: int):
+ """Namespace metadata generator."""
+ return {
+ "id": id,
+ "name": "Diaspora",
+ "path": "diaspora",
+ "kind": "group",
+ "full_path": "diaspora",
+ "parent_id": None,
+ "avatar_url": None,
+ "web_url": "https://gitlab.example.com/diaspora",
+ }
+
+
+def gitlab_project_metadata(
+ id: int, full_name: str, default_branch: str, description: str | None
+):
+ """Project metadata generator."""
+ return {
+ "id": id,
+ "description": description,
+ "name": "Diaspora Client",
+ "name_with_namespace": "Diaspora / Diaspora Client",
+ "path": "diaspora-client",
+ "path_with_namespace": full_name,
+ "created_at": "2013-09-30T13:46:02Z",
+ "default_branch": default_branch,
+ "tag_list": ["example", "disapora client"],
+ "topics": ["example", "disapora client"],
+ "ssh_url_to_repo": "git@gitlab.example.com:%s.git" % full_name,
+ "http_url_to_repo": "https://gitlab.example.com/%s.git" % full_name,
+ "web_url": "https://gitlab.example.com/%s" % full_name,
+ "avatar_url": "https://gitlab.example.com/uploads/project/avatar/%d/uploads/avatar.png"
+ % id,
+ "star_count": 0,
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "visibility": "public",
+ "namespace": gitlab_namespace_metadata(1),
+ }
+
+
+def gitlab_contributor_metadata(
+ email: str, contribution_count: int | None, name: str | None = "Example"
+):
+ """Contributor metadata generator."""
+ return {
+ "name": name,
+ "email": email,
+ "commits": contribution_count,
+ "additions": 0,
+ "deletions": 0,
+ }
+
+
+def gitlab_user_metadata(id: int, username: str, name: str | None):
+ """User metadata generator."""
+ return {
+ "id": id,
+ "username": username,
+ "name": name,
+ "state": "active",
+ "locked": False,
+ "avatar_url": "https://gitlab.example.com/uploads/user/avatar/%d/cd8.jpeg" % id,
+ "web_url": "https://gitlab.example.com/%s" % username,
+ }
+
+
+def gitlab_webhook_metadata(
+ id: int,
+ project_id: int,
+ url: str,
+):
+ """Webhook metadata generator."""
+ return {
+ "id": id,
+ "url": url,
+ "name": "Hook name",
+ "description": "Hook description",
+ "project_id": project_id,
+ "push_events": True,
+ "push_events_branch_filter": "",
+ "issues_events": True,
+ "confidential_issues_events": True,
+ "merge_requests_events": True,
+ "tag_push_events": True,
+ "note_events": True,
+ "confidential_note_events": True,
+ "job_events": True,
+ "pipeline_events": True,
+ "wiki_page_events": True,
+ "deployment_events": True,
+ "releases_events": True,
+ "milestone_events": True,
+ "feature_flag_events": True,
+ "enable_ssl_verification": True,
+ "repository_update_events": True,
+ "alert_status": "executable",
+ "disabled_until": None,
+ "url_variables": [],
+ "created_at": "2012-10-12T17:04:47Z",
+ "resource_access_token_events": True,
+ "custom_webhook_template": '{"event":"{{object_kind}}"}',
+ "custom_headers": [{"key": "Authorization"}],
+ }
+
+
+def gitlab_project_member_metadata(id: int, username: str, access_level: int):
+ """Project member metadata generator."""
+ return {
+ "id": id,
+ "username": username,
+ "name": "Raymond Smith",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root",
+ "created_at": "2012-09-22T14:13:35Z",
+ "created_by": {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
+ "web_url": "http://192.168.1.8:3000/root",
+ },
+ "expires_at": "2012-10-22",
+ "access_level": access_level,
+ "group_saml_identity": None,
+ }
+
+
+def gitlab_webhook_payload(
+ id: int,
+ tag_name: str,
+ release_name: str | None,
+ release_description: str | None,
+ project_id: int,
+ project_full_name: str,
+ project_default_branch: str,
+ project_description: str | None,
+):
+ """Return a sample webhook payload."""
+ return {
+ "id": id,
+ "created_at": "2020-11-02 12:55:12 UTC",
+ "description": release_description,
+ "name": release_name,
+ "released_at": "2020-11-02 12:55:12 UTC",
+ "tag": tag_name,
+ "object_kind": "release",
+ "project": gitlab_project_metadata(
+ project_id, project_full_name, project_default_branch, project_description
+ ),
+ "url": "https://example.com/gitlab-org/release-webhook-example/-/releases/v1.1",
+ "action": "create",
+ "assets": {
+ "count": 5,
+ "links": [
+ {
+ "id": 1,
+ "link_type": "other",
+ "name": "Changelog",
+ "url": "https://example.net/changelog",
+ }
+ ],
+ "sources": [
+ {
+ "format": "zip",
+ "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.zip",
+ },
+ {
+ "format": "tar.gz",
+ "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.gz",
+ },
+ {
+ "format": "tar.bz2",
+ "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.bz2",
+ },
+ {
+ "format": "tar",
+ "url": "https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar",
+ },
+ ],
+ },
+ "commit": {
+ "id": "ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8",
+ "message": "Release v1.1",
+ "title": "Release v1.1",
+ "timestamp": "2020-10-31T14:58:32+11:00",
+ "url": "https://example.com/gitlab-org/release-webhook-example/-/commit/ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8",
+ "author": {"name": "Example User", "email": "user@example.com"},
+ },
+ }
+
+
+class GitLabPatcher(TestProviderPatcher):
+ """Patch the GitLab API primitives to avoid real API calls and return test data instead."""
+
+ @staticmethod
+ def provider_factory() -> RepositoryServiceProviderFactory:
+ """GitLab provider factory."""
+ return GitLabProviderFactory(
+ base_url="https://gitlab.com",
+ webhook_receiver_url="http://localhost:5000/api/receivers/github/events/?access_token={token}",
+ )
+
+ @staticmethod
+ def test_webhook_payload(
+ generic_repository: GenericRepository,
+ generic_release: GenericRelease,
+ generic_repo_owner: GenericOwner,
+ ) -> dict[str, Any]:
+ """Return a sample webhook payload."""
+ return gitlab_webhook_payload(
+ int(generic_release.id),
+ generic_release.tag_name,
+ generic_release.name,
+ generic_release.body,
+ int(generic_repository.id),
+ generic_repository.full_name,
+ generic_repository.default_branch,
+ generic_repository.description,
+ )
+
+ def patch(
+ self,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_contributors: list[GenericContributor],
+ test_collaborators: list[dict[str, Any]],
+ test_generic_webhooks: list[GenericWebhook],
+ test_generic_user: GenericUser,
+ test_file: dict[str, Any],
+ ) -> Iterator[RepositoryServiceProvider]:
+ """Configure the patch and yield within the patched context."""
+ mock_gl = MagicMock()
+ mock_gl.projects = MagicMock()
+ mock_gl.users = MagicMock()
+ mock_gl.namespaces = MagicMock()
+
+ # We need contributors to correspond to users for the search operation.
+ # But the list should also contain the main test user.
+ test_user_email = "%s@inveniosoftware.org" % test_generic_user.username
+ test_user = gitlab.v4.objects.User(
+ mock_gl.users,
+ gitlab_user_metadata(
+ int(test_generic_user.id),
+ test_generic_user.username,
+ test_generic_user.display_name,
+ ),
+ )
+ # The email isn't returned in the API response (see https://docs.gitlab.com/api/users/#as-a-regular-user)
+ # so we store it separately here for querying.
+ users: dict[str, gitlab.v4.objects.User] = {test_user_email: test_user}
+ mock_gl.user = test_user
+
+ project_members: list[gitlab.v4.objects.ProjectMemberAll] = []
+ for collaborator in test_collaborators:
+ project_members.append(
+ gitlab.v4.objects.ProjectMemberAll(
+ mock_gl.projects,
+ gitlab_project_member_metadata(
+ int(collaborator["id"]),
+ collaborator["username"],
+ (
+ gitlab.const.MAINTAINER_ACCESS
+ if collaborator["admin"]
+ else gitlab.const.GUEST_ACCESS
+ ),
+ ),
+ )
+ )
+
+ # Some lesser-used API routes return dicts instead of dedicated objects
+ contributors: list[dict[str, Any]] = []
+ for generic_contributor in test_generic_contributors:
+ contributor_email = "%s@inveniosoftware.org" % generic_contributor.username
+ contributors.append(
+ gitlab_contributor_metadata(
+ contributor_email,
+ generic_contributor.contributions_count,
+ generic_contributor.display_name,
+ )
+ )
+ users[contributor_email] = gitlab.v4.objects.User(
+ mock_gl.users,
+ gitlab_user_metadata(
+ int(generic_contributor.id),
+ generic_contributor.username,
+ generic_contributor.display_name,
+ ),
+ )
+
+ def mock_users_list(search: str | None = None):
+ if search is None:
+ return users
+ return [users[search]]
+
+ mock_gl.users.list = MagicMock(side_effect=mock_users_list)
+
+ # We need to globally override this property because the method is provided as a
+ # property within a mixin which cannot be overriden on the instance level.
+ Project = gitlab.v4.objects.Project
+ Project.repository_contributors = MagicMock(return_value=contributors)
+
+ projs: dict[int, gitlab.v4.objects.Project] = {}
+ for generic_repo in test_generic_repositories:
+ proj = Project(
+ mock_gl.projects,
+ gitlab_project_metadata(
+ int(generic_repo.id),
+ generic_repo.full_name,
+ generic_repo.default_branch,
+ generic_repo.description,
+ ),
+ )
+
+ hooks: list[gitlab.v4.objects.ProjectHook] = []
+ for hook in test_generic_webhooks:
+ if hook.id != generic_repo.id:
+ continue
+
+ hooks.append(
+ gitlab.v4.objects.ProjectHook(
+ mock_gl.projects,
+ gitlab_webhook_metadata(
+ int(hook.id), int(generic_repo.id), hook.url
+ ),
+ )
+ )
+
+ proj.hooks = MagicMock()
+ proj.hooks.list = MagicMock(return_value=hooks)
+ new_hook = MagicMock()
+ new_hook.id = 12345
+ proj.hooks.create = MagicMock(return_value=new_hook)
+ proj.hooks.delete = MagicMock()
+
+ proj.members_all = MagicMock()
+ proj.members_all.list = MagicMock(return_value=project_members)
+
+ def mock_get_file(file_path: str, ref: str):
+ if file_path == test_file["path"]:
+ file = MagicMock()
+ file.decode = MagicMock(
+ return_value=test_file["content"].encode("ascii")
+ )
+ return file
+ else:
+ raise gitlab.GitlabGetError()
+
+ proj.files = MagicMock()
+ proj.files.get = MagicMock(side_effect=mock_get_file)
+
+ projs[int(generic_repo.id)] = proj
+
+ def mock_projects_get(id: int, lazy=False):
+ """We need to take the lazy param even though we ignore it."""
+ return projs[id]
+
+ mock_gl.projects.list = MagicMock(return_value=projs.values())
+ mock_gl.projects.get = MagicMock(side_effect=mock_projects_get)
+
+ with patch("invenio_vcs.contrib.gitlab.GitLabProvider._gl", new=mock_gl):
+ yield self.provider
diff --git a/tests/contrib_fixtures/patcher.py b/tests/contrib_fixtures/patcher.py
new file mode 100644
index 00000000..bcada365
--- /dev/null
+++ b/tests/contrib_fixtures/patcher.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# This file is part of Invenio.
+# Copyright (C) 2025 CERN.
+#
+# Invenio is free software; you can redistribute it and/or modify it
+# under the terms of the MIT License; see LICENSE file for more details.
+"""Abstract provider-specific patcher class."""
+
+from abc import ABC, abstractmethod
+from typing import Any, Iterator
+
+from invenio_vcs.generic_models import (
+ GenericContributor,
+ GenericOwner,
+ GenericRelease,
+ GenericRepository,
+ GenericUser,
+ GenericWebhook,
+)
+from invenio_vcs.providers import (
+ RepositoryServiceProvider,
+ RepositoryServiceProviderFactory,
+)
+
+
+class TestProviderPatcher(ABC):
+ """Interface for specifying a provider-specific primitive API patch and other test helpers."""
+
+ def __init__(self, test_user) -> None:
+ """Constructor."""
+ self.provider = self.provider_factory().for_user(test_user.id)
+
+ @staticmethod
+ @abstractmethod
+ def provider_factory() -> RepositoryServiceProviderFactory:
+ """Return the factory for the provider."""
+ raise NotImplementedError
+
+ @staticmethod
+ @abstractmethod
+ def test_webhook_payload(
+ generic_repository: GenericRepository,
+ generic_release: GenericRelease,
+ generic_repo_owner: GenericOwner,
+ ) -> dict[str, Any]:
+ """Return an example webhook payload."""
+ raise NotImplementedError
+
+ @abstractmethod
+ def patch(
+ self,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_contributors: list[GenericContributor],
+ test_collaborators: list[dict[str, Any]],
+ test_generic_webhooks: list[GenericWebhook],
+ test_generic_user: GenericUser,
+ test_file: dict[str, Any],
+ ) -> Iterator[RepositoryServiceProvider]:
+ """Implement the patch.
+
+ This should be applied to the primitives of the provider's API and not to e.g. the provider methods
+ themselves, as that would eliminate the purpose of testing the provider functionality.
+
+ At the end, this should yield within the patch context to ensure the patch is applied throughout the
+ test case run and then unapplied at the end for consistency.
+ """
+ raise NotImplementedError
diff --git a/tests/fixtures.py b/tests/fixtures.py
index ce4020a7..ff41d7dc 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -1,46 +1,25 @@
# -*- coding: utf-8 -*-
#
-# This file is part of Invenio.
-# Copyright (C) 2023 CERN.
+# Copyright (C) 2023-2025 CERN.
#
-# Invenio is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Invenio is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Invenio. If not, see .
-#
-# In applying this licence, CERN does not waive the privileges and immunities
-# granted to it by virtue of its status as an Intergovernmental Organization
-# or submit itself to any jurisdiction.
-
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
"""Define fixtures for tests."""
-import os
-from base64 import b64encode
-from zipfile import ZipFile
-
-from six import BytesIO
-from invenio_github.api import GitHubRelease
-from invenio_github.models import ReleaseStatus
+from invenio_vcs.models import ReleaseStatus
+from invenio_vcs.service import VCSRelease
-class TestGithubRelease(GitHubRelease):
- """Implements GithubRelease with test methods."""
+class TestVCSRelease(VCSRelease):
+ """Implements VCSRelease with test methods."""
def publish(self):
"""Sets release status to published.
Does not create a "real" record, as this only used to test the API.
"""
- self.release_object.status = ReleaseStatus.PUBLISHED
- self.release_object.record_id = "445aaacd-9de1-41ab-af52-25ab6cb93df7"
+ self.db_release.status = ReleaseStatus.PUBLISHED
+ self.db_release.record_id = "445aaacd-9de1-41ab-af52-25ab6cb93df7"
return {}
def process_release(self):
@@ -55,439 +34,12 @@ def resolve_record(self):
"""
return {}
+ @property
+ def badge_title(self):
+ """Test title for the badge."""
+ return "DOI"
-#
-# Fixture generators
-#
-def github_user_metadata(login, email=None, bio=True):
- """Github user fixture generator."""
- username = login
-
- user = {
- "avatar_url": "https://avatars.githubusercontent.com/u/7533764?",
- "collaborators": 0,
- "created_at": "2014-05-09T12:26:44Z",
- "disk_usage": 0,
- "events_url": "https://api.github.com/users/%s/events{/privacy}" % username,
- "followers": 0,
- "followers_url": "https://api.github.com/users/%s/followers" % username,
- "following": 0,
- "following_url": "https://api.github.com/users/%s/"
- "following{/other_user}" % username,
- "gists_url": "https://api.github.com/users/%s/gists{/gist_id}" % username,
- "gravatar_id": "12345678",
- "html_url": "https://github.com/%s" % username,
- "id": 1234,
- "login": "%s" % username,
- "organizations_url": "https://api.github.com/users/%s/orgs" % username,
- "owned_private_repos": 0,
- "plan": {
- "collaborators": 0,
- "name": "free",
- "private_repos": 0,
- "space": 307200,
- },
- "private_gists": 0,
- "public_gists": 0,
- "public_repos": 0,
- "received_events_url": "https://api.github.com/users/%s/"
- "received_events" % username,
- "repos_url": "https://api.github.com/users/%s/repos" % username,
- "site_admin": False,
- "starred_url": "https://api.github.com/users/%s/"
- "starred{/owner}{/repo}" % username,
- "subscriptions_url": "https://api.github.com/users/%s/"
- "subscriptions" % username,
- "total_private_repos": 0,
- "type": "User",
- "updated_at": "2014-05-09T12:26:44Z",
- "url": "https://api.github.com/users/%s" % username,
- "hireable": False,
- "location": "Geneve",
- }
-
- if bio:
- user.update(
- {
- "bio": "Software Engineer at CERN",
- "blog": "http://www.cern.ch",
- "company": "CERN",
- "name": "Lars Holm Nielsen",
- }
- )
-
- if email is not None:
- user.update(
- {
- "email": email,
- }
- )
-
- return user
-
-
-def github_repo_metadata(owner, repo, repo_id):
- """Github repository fixture generator."""
- repo_url = "%s/%s" % (owner, repo)
-
- return {
- "archive_url": "https://api.github.com/repos/%s/"
- "{archive_format}{/ref}" % repo_url,
- "assignees_url": "https://api.github.com/repos/%s/"
- "assignees{/user}" % repo_url,
- "blobs_url": "https://api.github.com/repos/%s/git/blobs{/sha}" % repo_url,
- "branches_url": "https://api.github.com/repos/%s/"
- "branches{/branch}" % repo_url,
- "clone_url": "https://github.com/%s.git" % repo_url,
- "collaborators_url": "https://api.github.com/repos/%s/"
- "collaborators{/collaborator}" % repo_url,
- "comments_url": "https://api.github.com/repos/%s/"
- "comments{/number}" % repo_url,
- "commits_url": "https://api.github.com/repos/%s/commits{/sha}" % repo_url,
- "compare_url": "https://api.github.com/repos/%s/compare/"
- "{base}...{head}" % repo_url,
- "contents_url": "https://api.github.com/repos/%s/contents/{+path}" % repo_url,
- "contributors_url": "https://api.github.com/repos/%s/contributors" % repo_url,
- "created_at": "2012-10-29T10:24:02Z",
- "default_branch": "master",
- "description": "",
- "downloads_url": "https://api.github.com/repos/%s/downloads" % repo_url,
- "events_url": "https://api.github.com/repos/%s/events" % repo_url,
- "fork": False,
- "forks": 0,
- "forks_count": 0,
- "forks_url": "https://api.github.com/repos/%s/forks" % repo_url,
- "full_name": repo_url,
- "git_commits_url": "https://api.github.com/repos/%s/git/"
- "commits{/sha}" % repo_url,
- "git_refs_url": "https://api.github.com/repos/%s/git/refs{/sha}" % repo_url,
- "git_tags_url": "https://api.github.com/repos/%s/git/tags{/sha}" % repo_url,
- "git_url": "git://github.com/%s.git" % repo_url,
- "has_downloads": True,
- "has_issues": True,
- "has_wiki": True,
- "homepage": None,
- "hooks_url": "https://api.github.com/repos/%s/hooks" % repo_url,
- "html_url": "https://github.com/%s" % repo_url,
- "id": repo_id,
- "issue_comment_url": "https://api.github.com/repos/%s/issues/"
- "comments/{number}" % repo_url,
- "issue_events_url": "https://api.github.com/repos/%s/issues/"
- "events{/number}" % repo_url,
- "issues_url": "https://api.github.com/repos/%s/issues{/number}" % repo_url,
- "keys_url": "https://api.github.com/repos/%s/keys{/key_id}" % repo_url,
- "labels_url": "https://api.github.com/repos/%s/labels{/name}" % repo_url,
- "language": None,
- "languages_url": "https://api.github.com/repos/%s/languages" % repo_url,
- "merges_url": "https://api.github.com/repos/%s/merges" % repo_url,
- "milestones_url": "https://api.github.com/repos/%s/"
- "milestones{/number}" % repo_url,
- "mirror_url": None,
- "name": "altantis-conf",
- "notifications_url": "https://api.github.com/repos/%s/"
- "notifications{?since,all,participating}",
- "open_issues": 0,
- "open_issues_count": 0,
- "owner": {
- "avatar_url": "https://avatars.githubusercontent.com/u/1234?",
- "events_url": "https://api.github.com/users/%s/" "events{/privacy}" % owner,
- "followers_url": "https://api.github.com/users/%s/followers" % owner,
- "following_url": "https://api.github.com/users/%s/"
- "following{/other_user}" % owner,
- "gists_url": "https://api.github.com/users/%s/gists{/gist_id}" % owner,
- "gravatar_id": "1234",
- "html_url": "https://github.com/%s" % owner,
- "id": 1698163,
- "login": "%s" % owner,
- "organizations_url": "https://api.github.com/users/%s/orgs" % owner,
- "received_events_url": "https://api.github.com/users/%s/"
- "received_events" % owner,
- "repos_url": "https://api.github.com/users/%s/repos" % owner,
- "site_admin": False,
- "starred_url": "https://api.github.com/users/%s/"
- "starred{/owner}{/repo}" % owner,
- "subscriptions_url": "https://api.github.com/users/%s/"
- "subscriptions" % owner,
- "type": "User",
- "url": "https://api.github.com/users/%s" % owner,
- },
- "permissions": {"admin": True, "pull": True, "push": True},
- "private": False,
- "pulls_url": "https://api.github.com/repos/%s/pulls{/number}" % repo_url,
- "pushed_at": "2012-10-29T10:28:08Z",
- "releases_url": "https://api.github.com/repos/%s/releases{/id}" % repo_url,
- "size": 104,
- "ssh_url": "git@github.com:%s.git" % repo_url,
- "stargazers_count": 0,
- "stargazers_url": "https://api.github.com/repos/%s/stargazers" % repo_url,
- "statuses_url": "https://api.github.com/repos/%s/statuses/{sha}" % repo_url,
- "subscribers_url": "https://api.github.com/repos/%s/subscribers" % repo_url,
- "subscription_url": "https://api.github.com/repos/%s/subscription" % repo_url,
- "svn_url": "https://github.com/%s" % repo_url,
- "tags_url": "https://api.github.com/repos/%s/tags" % repo_url,
- "teams_url": "https://api.github.com/repos/%s/teams" % repo_url,
- "trees_url": "https://api.github.com/repos/%s/git/trees{/sha}" % repo_url,
- "updated_at": "2013-10-25T11:30:04Z",
- "url": "https://api.github.com/repos/%s" % repo_url,
- "watchers": 0,
- "watchers_count": 0,
- "deployments_url": "https://api.github.com/repos/%s/deployments" % repo_url,
- "archived": False,
- "has_pages": False,
- "has_projects": False,
- "network_count": 0,
- "subscribers_count": 0,
- }
-
-
-def ZIPBALL():
- """Github repository ZIP fixture."""
- memfile = BytesIO()
- zipfile = ZipFile(memfile, "w")
- zipfile.writestr("test.txt", "hello world")
- zipfile.close()
- memfile.seek(0)
- return memfile
-
-
-def PAYLOAD(sender, repo, repo_id, tag="v1.0"):
- """Github payload fixture generator."""
- c = dict(repo=repo, user=sender, url="%s/%s" % (sender, repo), id="4321", tag=tag)
-
- return {
- "action": "published",
- "release": {
- "url": "https://api.github.com/repos/%(url)s/releases/%(id)s" % c,
- "assets_url": "https://api.github.com/repos/%(url)s/releases/"
- "%(id)s/assets" % c,
- "upload_url": "https://uploads.github.com/repos/%(url)s/"
- "releases/%(id)s/assets{?name}" % c,
- "html_url": "https://github.com/%(url)s/releases/tag/%(tag)s" % c,
- "id": int(c["id"]),
- "tag_name": c["tag"],
- "target_commitish": "master",
- "name": "Release name",
- "body": "",
- "draft": False,
- "author": {
- "login": "%(user)s" % c,
- "id": 1698163,
- "avatar_url": "https://avatars.githubusercontent.com/u/12345",
- "gravatar_id": "12345678",
- "url": "https://api.github.com/users/%(user)s" % c,
- "html_url": "https://github.com/%(user)s" % c,
- "followers_url": "https://api.github.com/users/%(user)s/"
- "followers" % c,
- "following_url": "https://api.github.com/users/%(user)s/"
- "following{/other_user}" % c,
- "gists_url": "https://api.github.com/users/%(user)s/"
- "gists{/gist_id}" % c,
- "starred_url": "https://api.github.com/users/%(user)s/"
- "starred{/owner}{/repo}" % c,
- "subscriptions_url": "https://api.github.com/users/%(user)s/"
- "subscriptions" % c,
- "organizations_url": "https://api.github.com/users/%(user)s/"
- "orgs" % c,
- "repos_url": "https://api.github.com/users/%(user)s/repos" % c,
- "events_url": "https://api.github.com/users/%(user)s/"
- "events{/privacy}" % c,
- "received_events_url": "https://api.github.com/users/"
- "%(user)s/received_events" % c,
- "type": "User",
- "site_admin": False,
- },
- "prerelease": False,
- "created_at": "2014-02-26T08:13:42Z",
- "published_at": "2014-02-28T13:55:32Z",
- "assets": [],
- "tarball_url": "https://api.github.com/repos/%(url)s/"
- "tarball/%(tag)s" % c,
- "zipball_url": "https://api.github.com/repos/%(url)s/"
- "zipball/%(tag)s" % c,
- },
- "repository": {
- "id": repo_id,
- "name": repo,
- "full_name": "%(url)s" % c,
- "owner": {
- "login": "%(user)s" % c,
- "id": 1698163,
- "avatar_url": "https://avatars.githubusercontent.com/u/" "1698163",
- "gravatar_id": "bbc951080061fc48cae0279d27f3c015",
- "url": "https://api.github.com/users/%(user)s" % c,
- "html_url": "https://github.com/%(user)s" % c,
- "followers_url": "https://api.github.com/users/%(user)s/"
- "followers" % c,
- "following_url": "https://api.github.com/users/%(user)s/"
- "following{/other_user}" % c,
- "gists_url": "https://api.github.com/users/%(user)s/"
- "gists{/gist_id}" % c,
- "starred_url": "https://api.github.com/users/%(user)s/"
- "starred{/owner}{/repo}" % c,
- "subscriptions_url": "https://api.github.com/users/%(user)s/"
- "subscriptions" % c,
- "organizations_url": "https://api.github.com/users/%(user)s/"
- "orgs" % c,
- "repos_url": "https://api.github.com/users/%(user)s/" "repos" % c,
- "events_url": "https://api.github.com/users/%(user)s/"
- "events{/privacy}" % c,
- "received_events_url": "https://api.github.com/users/"
- "%(user)s/received_events" % c,
- "type": "User",
- "site_admin": False,
- },
- "private": False,
- "html_url": "https://github.com/%(url)s" % c,
- "description": "Repo description.",
- "fork": True,
- "url": "https://api.github.com/repos/%(url)s" % c,
- "forks_url": "https://api.github.com/repos/%(url)s/forks" % c,
- "keys_url": "https://api.github.com/repos/%(url)s/" "keys{/key_id}" % c,
- "collaborators_url": "https://api.github.com/repos/%(url)s/"
- "collaborators{/collaborator}" % c,
- "teams_url": "https://api.github.com/repos/%(url)s/teams" % c,
- "hooks_url": "https://api.github.com/repos/%(url)s/hooks" % c,
- "issue_events_url": "https://api.github.com/repos/%(url)s/"
- "issues/events{/number}" % c,
- "events_url": "https://api.github.com/repos/%(url)s/events" % c,
- "assignees_url": "https://api.github.com/repos/%(url)s/"
- "assignees{/user}" % c,
- "branches_url": "https://api.github.com/repos/%(url)s/"
- "branches{/branch}" % c,
- "tags_url": "https://api.github.com/repos/%(url)s/tags" % c,
- "blobs_url": "https://api.github.com/repos/%(url)s/git/" "blobs{/sha}" % c,
- "git_tags_url": "https://api.github.com/repos/%(url)s/git/"
- "tags{/sha}" % c,
- "git_refs_url": "https://api.github.com/repos/%(url)s/git/"
- "refs{/sha}" % c,
- "trees_url": "https://api.github.com/repos/%(url)s/git/" "trees{/sha}" % c,
- "statuses_url": "https://api.github.com/repos/%(url)s/"
- "statuses/{sha}" % c,
- "languages_url": "https://api.github.com/repos/%(url)s/" "languages" % c,
- "stargazers_url": "https://api.github.com/repos/%(url)s/" "stargazers" % c,
- "contributors_url": "https://api.github.com/repos/%(url)s/"
- "contributors" % c,
- "subscribers_url": "https://api.github.com/repos/%(url)s/"
- "subscribers" % c,
- "subscription_url": "https://api.github.com/repos/%(url)s/"
- "subscription" % c,
- "commits_url": "https://api.github.com/repos/%(url)s/" "commits{/sha}" % c,
- "git_commits_url": "https://api.github.com/repos/%(url)s/git/"
- "commits{/sha}" % c,
- "comments_url": "https://api.github.com/repos/%(url)s/"
- "comments{/number}" % c,
- "issue_comment_url": "https://api.github.com/repos/%(url)s/"
- "issues/comments/{number}" % c,
- "contents_url": "https://api.github.com/repos/%(url)s/"
- "contents/{+path}" % c,
- "compare_url": "https://api.github.com/repos/%(url)s/"
- "compare/{base}...{head}" % c,
- "merges_url": "https://api.github.com/repos/%(url)s/merges" % c,
- "archive_url": "https://api.github.com/repos/%(url)s/"
- "{archive_format}{/ref}" % c,
- "downloads_url": "https://api.github.com/repos/%(url)s/" "downloads" % c,
- "issues_url": "https://api.github.com/repos/%(url)s/" "issues{/number}" % c,
- "pulls_url": "https://api.github.com/repos/%(url)s/" "pulls{/number}" % c,
- "milestones_url": "https://api.github.com/repos/%(url)s/"
- "milestones{/number}" % c,
- "notifications_url": "https://api.github.com/repos/%(url)s/"
- "notifications{?since,all,participating}" % c,
- "labels_url": "https://api.github.com/repos/%(url)s/" "labels{/name}" % c,
- "releases_url": "https://api.github.com/repos/%(url)s/" "releases{/id}" % c,
- "created_at": "2014-02-26T07:39:11Z",
- "updated_at": "2014-02-28T13:55:32Z",
- "pushed_at": "2014-02-28T13:55:32Z",
- "git_url": "git://github.com/%(url)s.git" % c,
- "ssh_url": "git@github.com:%(url)s.git" % c,
- "clone_url": "https://github.com/%(url)s.git" % c,
- "svn_url": "https://github.com/%(url)s" % c,
- "homepage": None,
- "size": 388,
- "stargazers_count": 0,
- "watchers_count": 0,
- "language": "Python",
- "has_issues": False,
- "has_downloads": True,
- "has_wiki": True,
- "forks_count": 0,
- "mirror_url": None,
- "open_issues_count": 0,
- "forks": 0,
- "open_issues": 0,
- "watchers": 0,
- "default_branch": "master",
- "master_branch": "master",
- },
- "sender": {
- "login": "%(user)s" % c,
- "id": 1698163,
- "avatar_url": "https://avatars.githubusercontent.com/u/1234578",
- "gravatar_id": "12345678",
- "url": "https://api.github.com/users/%(user)s" % c,
- "html_url": "https://github.com/%(user)s" % c,
- "followers_url": "https://api.github.com/users/%(user)s/" "followers" % c,
- "following_url": "https://api.github.com/users/%(user)s/"
- "following{/other_user}" % c,
- "gists_url": "https://api.github.com/users/%(user)s/" "gists{/gist_id}" % c,
- "starred_url": "https://api.github.com/users/%(user)s/"
- "starred{/owner}{/repo}" % c,
- "subscriptions_url": "https://api.github.com/users/%(user)s/"
- "subscriptions" % c,
- "organizations_url": "https://api.github.com/users/%(user)s/" "orgs" % c,
- "repos_url": "https://api.github.com/users/%(user)s/repos" % c,
- "events_url": "https://api.github.com/users/%(user)s/"
- "events{/privacy}" % c,
- "received_events_url": "https://api.github.com/users/%(user)s/"
- "received_events" % c,
- "type": "User",
- "site_admin": False,
- },
- }
-
-
-def ORG(login):
- """Github organization fixture generator."""
- return {
- "login": login,
- "id": 1234,
- "url": "https://api.github.com/orgs/%s" % login,
- "repos_url": "https://api.github.com/orgs/%s/repos" % login,
- "events_url": "https://api.github.com/orgs/%s/events" % login,
- "members_url": "https://api.github.com/orgs/%s/" "members{/member}" % login,
- "public_members_url": "https://api.github.com/orgs/%s/"
- "public_members{/member}" % login,
- "avatar_url": "https://avatars.githubusercontent.com/u/1234?",
- }
-
-
-def github_file_contents(owner, repo, file_path, ref, data):
- """Github content fixture generator."""
- c = dict(
- url="%s/%s" % (owner, repo),
- owner=owner,
- repo=repo,
- file=file_path,
- ref=ref,
- )
-
- return {
- "_links": {
- "git": "https://api.github.com/repos/%(url)s/git/blobs/"
- "aaaffdfbead0b67bd6a5f5819c458a1215ecb0f6" % c,
- "html": "https://github.com/%(url)s/blob/%(ref)s/%(file)s" % c,
- "self": "https://api.github.com/repos/%(url)s/contents/"
- "%(file)s?ref=%(ref)s" % c,
- },
- "content": b64encode(data),
- "encoding": "base64",
- "git_url": "https://api.github.com/repos/%(url)s/git/blobs/"
- "aaaffdfbead0b67bd6a5f5819c458a1215ecb0f6" % c,
- "html_url": "https://github.com/%(url)s/blob/%(ref)s/%(file)s" % c,
- "name": os.path.basename(file_path),
- "path": file_path,
- "sha": "aaaffdfbead0b67bd6a5f5819c458a1215ecb0f6",
- "size": 1209,
- "type": "file",
- "url": "https://api.github.com/repos/%(url)s/contents/"
- "%(file)s?ref=%(ref)s" % c,
- }
+ @property
+ def badge_value(self):
+ """Test value for the badge."""
+ return self.db_release.tag
diff --git a/tests/test_alembic.py b/tests/test_alembic.py
index 79f82902..3d5fa48f 100644
--- a/tests/test_alembic.py
+++ b/tests/test_alembic.py
@@ -3,9 +3,9 @@
# Copyright (C) 2023 CERN.
# Copyright (C) 2024 Graz University of Technology.
#
-# Invenio-Github is free software; you can redistribute it and/or modify
+# Invenio-vcs is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.
-"""Test invenio-github alembic."""
+"""Test invenio-vcs alembic."""
import pytest
from invenio_db.utils import alembic_test_context, drop_alembic_version_table
@@ -23,8 +23,9 @@ def test_alembic(base_app, database):
# Check that this package's SQLAlchemy models have been properly registered
tables = [x for x in db.metadata.tables]
- assert "github_repositories" in tables
- assert "github_releases" in tables
+ assert "vcs_repositories" in tables
+ assert "vcs_releases" in tables
+ assert "vcs_repository_users" in tables
# Check that Alembic agrees that there's no further tables to create.
assert len(ext.alembic.compare_metadata()) == 0
diff --git a/tests/test_api.py b/tests/test_api.py
deleted file mode 100644
index 44389d3f..00000000
--- a/tests/test_api.py
+++ /dev/null
@@ -1,119 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# Copyright (C) 2023-2025 CERN.
-#
-# Invenio-Github is free software; you can redistribute it and/or modify
-# it under the terms of the MIT License; see LICENSE file for more details.
-"""Test invenio-github api."""
-
-import json
-
-import pytest
-from invenio_webhooks.models import Event
-
-from invenio_github.api import GitHubAPI, GitHubRelease
-from invenio_github.models import Release, ReleaseStatus
-
-from .fixtures import PAYLOAD as github_payload_fixture
-
-# GithubAPI tests
-
-
-def test_github_api_create_hook(app, test_user, github_api):
- """Test hook creation."""
- api = GitHubAPI(test_user.id)
- api.init_account()
- repo_id = 1
- repo_name = "repo-1"
- hook_created = api.create_hook(repo_id=repo_id, repo_name=repo_name)
- assert hook_created
-
-
-# GithubRelease api tests
-
-
-def test_release_api(app, test_user, github_api):
- api = GitHubAPI(test_user.id)
- api.init_account()
- repo_id = 2
- repo_name = "repo-2"
-
- # Create a repo hook
- hook_created = api.create_hook(repo_id=repo_id, repo_name=repo_name)
- assert hook_created
-
- headers = [("Content-Type", "application/json")]
-
- payload = github_payload_fixture("auser", repo_name, repo_id, tag="v1.0")
- with app.test_request_context(headers=headers, data=json.dumps(payload)):
- event = Event.create(
- receiver_id="github",
- user_id=test_user.id,
- )
- release = Release(
- release_id=payload["release"]["id"],
- tag=event.payload["release"]["tag_name"],
- repository_id=repo_id,
- event=event,
- status=ReleaseStatus.RECEIVED,
- )
- # Idea is to test the public interface of GithubRelease
- gh = GitHubRelease(release)
-
- # Validate that public methods raise NotImplementedError
- with pytest.raises(NotImplementedError):
- gh.process_release()
-
- with pytest.raises(NotImplementedError):
- gh.publish()
-
- assert getattr(gh, "retrieve_remote_file") is not None
-
- # Validate that an invalid file returns None
- invalid_remote_file_contents = gh.retrieve_remote_file("test")
-
- assert invalid_remote_file_contents is None
-
- # Validate that a valid file returns its data
- valid_remote_file_contents = gh.retrieve_remote_file("test.py")
-
- assert valid_remote_file_contents is not None
- assert valid_remote_file_contents.decoded["name"] == "test.py"
-
-
-def test_release_branch_tag_conflict(app, test_user, github_api):
- api = GitHubAPI(test_user.id)
- api.init_account()
- repo_id = 2
- repo_name = "repo-2"
-
- # Create a repo hook
- hook_created = api.create_hook(repo_id=repo_id, repo_name=repo_name)
- assert hook_created
-
- headers = [("Content-Type", "application/json")]
-
- payload = github_payload_fixture(
- "auser", repo_name, repo_id, tag="v1.0-tag-and-branch"
- )
- with app.test_request_context(headers=headers, data=json.dumps(payload)):
- event = Event.create(
- receiver_id="github",
- user_id=test_user.id,
- )
- release = Release(
- release_id=payload["release"]["id"],
- tag=event.payload["release"]["tag_name"],
- repository_id=repo_id,
- event=event,
- status=ReleaseStatus.RECEIVED,
- )
- # Idea is to test the public interface of GithubRelease
- rel_api = GitHubRelease(release)
- resolved_url = rel_api.resolve_zipball_url()
- ref_tag_url = (
- "https://github.com/auser/repo-2/zipball/refs/tags/v1.0-tag-and-branch"
- )
- assert resolved_url == ref_tag_url
- # Check that the original zipball URL from the event payload is not the same
- assert rel_api.release_zipball_url != ref_tag_url
diff --git a/tests/test_badge.py b/tests/test_badge.py
index ae91319b..76d42d94 100644
--- a/tests/test_badge.py
+++ b/tests/test_badge.py
@@ -1,43 +1,100 @@
# -*- coding: utf-8 -*-
#
-# This file is part of Invenio.
-# Copyright (C) 2015, 2016 CERN.
+# Copyright (C) 2023-2025 CERN.
#
-# Invenio is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Invenio is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Invenio. If not, see .
-#
-# In applying this licence, CERN does not waive the privileges and immunities
-# granted to it by virtue of its status as an Intergovernmental Organization
-# or submit itself to any jurisdiction.
-
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
"""Test cases for badge creation."""
from __future__ import absolute_import
+from unittest.mock import patch
+
+import pytest
from flask import url_for
+from flask_login import login_user
+from invenio_accounts.testutils import login_user_via_session
+from invenio_webhooks.models import Event
+
+from invenio_vcs.generic_models import GenericRelease, GenericRepository
+from invenio_vcs.models import Release, ReleaseStatus, Repository
+from invenio_vcs.service import VCSService
+
+
+@pytest.mark.skip(reason="Unit tests for UI routes are unimplemented.")
+def test_badge_views(
+ app,
+ db,
+ client,
+ test_user,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_release: GenericRelease,
+ vcs_service: VCSService,
+):
+ """Test create_badge method."""
+ vcs_service.sync(hooks=False)
+ generic_repo = test_generic_repositories[0]
+ db_repo = Repository.get(
+ provider=vcs_service.provider.factory.id, provider_id=generic_repo.id
+ )
+ db_repo.enabled_by_user_id = test_user.id
+ db.session.add(db_repo)
+
+ event = Event(
+ # Receiver ID is same as provider ID
+ receiver_id=vcs_service.provider.factory.id,
+ user_id=test_user.id,
+ payload={},
+ )
+
+ db_release = Release(
+ provider=vcs_service.provider.factory.id,
+ provider_id=test_generic_release.id,
+ tag=test_generic_release.tag_name,
+ repository=db_repo,
+ event=event,
+ status=ReleaseStatus.PUBLISHED,
+ )
+ db.session.add(db_release)
+ db.session.commit()
+
+ login_user(test_user)
+ login_user_via_session(client, email=test_user.email)
+
+ def mock_url_for(target: str, **kwargs):
+ """The badge route handler calls url_for referencing a module we don't have access to during the test run.
+
+ Testing the functionality of that module is out of scope here.
+ """
+ return "https://example.com"
+
+ with patch("invenio_vcs.views.badge.url_for", mock_url_for):
+ badge_url = url_for(
+ "invenio_vcs_badge.index",
+ provider=vcs_service.provider.factory.id,
+ repo_provider_id=generic_repo.id,
+ )
+ badge_resp = client.get(badge_url)
+ # Expect a redirect to the badge formatter
+ assert badge_resp.status_code == 302
+
+ class TestAbortException(Exception):
+ def __init__(self, code: int) -> None:
+ self.code = code
+
+ # Test with non-existent provider id
+ with patch(
+ "invenio_vcs.views.badge.abort",
+ # This would crash with the actual abort function as it would try to render the 404 Jinja
+ # template which is not available during tests.
+ lambda code: (_ for _ in ()).throw(TestAbortException(code)),
+ ):
+ badge_url = url_for(
+ "invenio_vcs_badge.index",
+ provider=vcs_service.provider.factory.id,
+ repo_provider_id="42",
+ )
+ with pytest.raises(TestAbortException) as e:
+ client.get(badge_url)
-# TODO uncomment when migrated
-# def test_badge_views(app, release_model):
-# """Test create_badge method."""
-# with app.test_client() as client:
-# badge_url = url_for(
-# "invenio_github_badge.index", github_id=release_model.release_id
-# )
-# badge_resp = client.get(badge_url)
-# assert release_model.record["doi"] in badge_resp.location
-
-# with app.test_client() as client:
-# # Test with non-existent github id
-# badge_url = url_for("invenio_github_badge.index", github_id=42)
-# badge_resp = client.get(badge_url)
-# assert badge_resp.status_code == 404
+ assert e.value.code == 404
diff --git a/tests/test_invenio_github.py b/tests/test_invenio_github.py
deleted file mode 100644
index ceed134a..00000000
--- a/tests/test_invenio_github.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# This file is part of Invenio.
-# Copyright (C) 2023 CERN.
-#
-# Invenio is free software; you can redistribute it
-# and/or modify it under the terms of the GNU General Public License as
-# published by the Free Software Foundation; either version 2 of the
-# License, or (at your option) any later version.
-#
-# Invenio is distributed in the hope that it will be
-# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-# General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Invenio; if not, write to the
-# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
-# MA 02111-1307, USA.
-#
-# In applying this license, CERN does not
-# waive the privileges and immunities granted to it by virtue of its status
-# as an Intergovernmental Organization or submit itself to any jurisdiction.
-
-
-"""Module tests."""
-
-from flask import Flask
-
-from invenio_github import InvenioGitHub
-
-
-def test_version():
- """Test version import."""
- from invenio_github import __version__
-
- assert __version__
-
-
-def test_init():
- """Test extension initialization."""
- app = Flask("testapp")
- ext = InvenioGitHub(app)
- assert "invenio-github" in app.extensions
-
- app = Flask("testapp")
- ext = InvenioGitHub()
- assert "invenio-github" not in app.extensions
- ext.init_app(app)
- assert "invenio-github" in app.extensions
diff --git a/tests/test_invenio_vcs.py b/tests/test_invenio_vcs.py
new file mode 100644
index 00000000..af033139
--- /dev/null
+++ b/tests/test_invenio_vcs.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2023-2025 CERN.
+#
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+"""Module tests."""
+
+from flask import Flask
+
+from invenio_vcs import InvenioVCS
+
+
+def test_version():
+ """Test version import."""
+ from invenio_vcs import __version__
+
+ assert __version__
+
+
+def test_init():
+ """Test extension initialization."""
+ app = Flask("testapp")
+ ext = InvenioVCS(app)
+ assert "invenio-vcs" in app.extensions
+
+ app = Flask("testapp")
+ ext = InvenioVCS()
+ assert "invenio-vcs" not in app.extensions
+ ext.init_app(app)
+ assert "invenio-vcs" in app.extensions
diff --git a/tests/test_models.py b/tests/test_models.py
index be63e40f..241b139f 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,30 +1,19 @@
# -*- coding: utf-8 -*-
#
-# This file is part of Invenio.
-# Copyright (C) 2023 CERN.
+# Copyright (C) 2023-2025 CERN.
#
-# Invenio is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Invenio is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Invenio. If not, see .
-#
-# In applying this licence, CERN does not waive the privileges and immunities
-# granted to it by virtue of its status as an Intergovernmental Organization
-# or submit itself to any jurisdiction.
-
-"""Test cases for badge creation."""
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+"""Test cases for VCS models."""
-from invenio_github.models import Repository
+from invenio_vcs.models import Repository
def test_repository_unbound(app):
- """Test create_badge method."""
- assert Repository(name="org/repo", github_id=1).latest_release() is None
+ """Test unbound repository."""
+ assert (
+ Repository(
+ full_name="org/repo", provider_id="1", provider="test"
+ ).latest_release()
+ is None
+ )
diff --git a/tests/test_provider.py b/tests/test_provider.py
new file mode 100644
index 00000000..ee3343c5
--- /dev/null
+++ b/tests/test_provider.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2025 CERN.
+#
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+"""Test invenio-vcs provider layer."""
+
+from invenio_vcs.generic_models import (
+ GenericContributor,
+ GenericRepository,
+ GenericWebhook,
+)
+from invenio_vcs.providers import RepositoryServiceProvider
+from invenio_vcs.service import VCSService
+
+
+def test_vcs_provider_list_repositories(
+ vcs_provider: RepositoryServiceProvider,
+ test_generic_repositories: list[GenericRepository],
+):
+ repos = vcs_provider.list_repositories()
+ assert repos is not None
+ assert len(repos) == len(test_generic_repositories)
+ assert isinstance(repos[test_generic_repositories[0].id], GenericRepository)
+
+
+def test_vcs_provider_list_hooks(
+ vcs_provider: RepositoryServiceProvider,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_webhooks: list[GenericWebhook],
+):
+ repo_id = test_generic_repositories[0].id
+ test_hooks = list(
+ filter(lambda h: h.repository_id == repo_id, test_generic_webhooks)
+ )
+ hooks = vcs_provider.list_repository_webhooks(repo_id)
+ assert hooks is not None
+ assert len(hooks) == len(test_hooks)
+ assert hooks[0].id == test_hooks[0].id
+
+
+def test_vcs_provider_list_user_ids(vcs_provider: RepositoryServiceProvider):
+ # This should correspond to the IDs in `test_collaborators` at least roughly
+ user_ids = vcs_provider.list_repository_user_ids("1")
+ assert user_ids is not None
+ # Only one user should have admin privileges
+ assert len(user_ids) == 1
+ assert user_ids[0] == "1"
+
+
+def test_vcs_provider_get_repository(vcs_provider: RepositoryServiceProvider):
+ repo = vcs_provider.get_repository("1")
+ assert repo is not None
+
+
+def test_vcs_provider_create_hook(
+ # For this test, we need to init accounts so we need to use the service
+ vcs_service: VCSService,
+):
+ repo_id = "1"
+ hook_created = vcs_service.provider.create_webhook(repository_id=repo_id)
+ assert hook_created is not None
+
+
+def test_vcs_provider_get_own_user(vcs_provider: RepositoryServiceProvider):
+ own_user = vcs_provider.get_own_user()
+ assert own_user is not None
+ assert own_user.id == "1"
+
+
+def test_vcs_provider_list_repository_contributors(
+ vcs_provider: RepositoryServiceProvider,
+ test_generic_contributors: list[GenericContributor],
+ test_generic_repositories: list[GenericRepository],
+):
+ contributors = vcs_provider.list_repository_contributors(
+ test_generic_repositories[0].id, 10
+ )
+ assert contributors is not None
+ assert len(contributors) == len(test_generic_contributors)
+ # The list order is arbitrary so we cannot validate that the IDs match up
+
+
+def test_vcs_provider_get_repository_owner(
+ vcs_provider: RepositoryServiceProvider,
+ test_generic_repositories: list[GenericRepository],
+):
+ owner = vcs_provider.get_repository_owner(test_generic_repositories[0].id)
+ assert owner is not None
+ # We don't store the owner id in the generic repository model
+ assert owner.id == "1"
diff --git a/tests/test_service.py b/tests/test_service.py
new file mode 100644
index 00000000..6e7a956b
--- /dev/null
+++ b/tests/test_service.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2023-2025 CERN.
+#
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+"""Test invenio-vcs service layer."""
+
+import json
+
+import pytest
+from invenio_webhooks.models import Event
+
+from invenio_vcs.generic_models import (
+ GenericOwner,
+ GenericRelease,
+ GenericRepository,
+)
+from invenio_vcs.models import Release, ReleaseStatus
+from invenio_vcs.service import VCSRelease, VCSService
+from tests.contrib_fixtures.patcher import TestProviderPatcher
+
+
+def test_vcs_service_user_repositories(
+ vcs_service: VCSService,
+ test_generic_repositories: list[GenericRepository],
+):
+ vcs_service.sync()
+
+ user_available_repositories = list(vcs_service.user_available_repositories)
+ assert len(user_available_repositories) == len(test_generic_repositories)
+
+ repo_id = test_generic_repositories[0].id
+ assert user_available_repositories[0].provider_id == repo_id
+
+ # We haven't enabled any repositories yet
+ user_enabled_repositories = list(vcs_service.user_enabled_repositories)
+ assert len(user_enabled_repositories) == 0
+
+ vcs_service.enable_repository(repo_id)
+ user_enabled_repositories = list(vcs_service.user_enabled_repositories)
+ assert len(user_enabled_repositories) == 1
+ assert user_enabled_repositories[0].provider_id == repo_id
+ assert user_enabled_repositories[0].hook is not None
+
+ vcs_service.disable_repository(repo_id)
+ user_enabled_repositories = list(vcs_service.user_enabled_repositories)
+ assert len(user_enabled_repositories) == 0
+
+
+def test_vcs_service_list_repos(vcs_service: VCSService):
+ vcs_service.sync()
+ repos = vcs_service.list_repositories()
+ assert len(repos) == 3
+
+
+def test_vcs_service_get_repo_default_branch(
+ vcs_service: VCSService, test_generic_repositories: list[GenericRepository]
+):
+ vcs_service.sync()
+ default_branch = vcs_service.get_repo_default_branch(
+ test_generic_repositories[0].id
+ )
+ assert default_branch == test_generic_repositories[0].default_branch
+
+
+def test_vcs_service_get_last_sync_time(vcs_service: VCSService):
+ vcs_service.sync()
+ last_sync_time = vcs_service.get_last_sync_time()
+ assert last_sync_time is not None
+
+
+def test_vcs_service_get_repository(
+ vcs_service: VCSService, test_generic_repositories: list[GenericRepository]
+):
+ vcs_service.sync()
+ repository = vcs_service.get_repository(test_generic_repositories[0].id)
+ assert repository is not None
+ assert repository.provider_id == test_generic_repositories[0].id
+
+
+def test_release_api(
+ app,
+ test_user,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_release: GenericRelease,
+ test_generic_owner: GenericOwner,
+ provider_patcher: TestProviderPatcher,
+ vcs_service: VCSService,
+):
+ repo = test_generic_repositories[0]
+ headers = [("Content-Type", "application/json")]
+
+ payload = provider_patcher.test_webhook_payload(
+ repo, test_generic_release, test_generic_owner
+ )
+ with app.test_request_context(headers=headers, data=json.dumps(payload)):
+ event = Event.create(
+ receiver_id=provider_patcher.provider_factory().id,
+ user_id=test_user.id,
+ )
+ release = Release(
+ provider_id=test_generic_release.id,
+ tag=test_generic_release.tag_name,
+ repository_id=repo.id,
+ event=event,
+ status=ReleaseStatus.RECEIVED,
+ )
+
+ # Idea is to test the public interface of VCSRelease
+ r = VCSRelease(release, vcs_service.provider)
+
+ # Validate that public methods raise NotImplementedError
+ with pytest.raises(NotImplementedError):
+ r.process_release()
+
+ with pytest.raises(NotImplementedError):
+ r.publish()
+
+ # Validate that an invalid file returns None
+ invalid_remote_file_contents = vcs_service.provider.retrieve_remote_file(
+ repo.id, release.tag, "test"
+ )
+
+ assert invalid_remote_file_contents is None
+
+ # Validate that a valid file returns its data
+ valid_remote_file_contents = vcs_service.provider.retrieve_remote_file(
+ repo.id, release.tag, "test.py"
+ )
+
+ assert valid_remote_file_contents is not None
+ assert isinstance(valid_remote_file_contents, bytes)
+
+
+"""
+
+def test_release_branch_tag_conflict(app, test_user, github_api):
+ api = GitHubAPI(test_user.id)
+ api.init_account()
+ repo_id = 2
+ repo_name = "repo-2"
+
+ # Create a repo hook
+ hook_created = api.create_hook(repo_id=repo_id, repo_name=repo_name)
+ assert hook_created
+
+ headers = [("Content-Type", "application/json")]
+
+ payload = github_payload_fixture(
+ "auser", repo_name, repo_id, tag="v1.0-tag-and-branch"
+ )
+ with app.test_request_context(headers=headers, data=json.dumps(payload)):
+ event = Event.create(
+ receiver_id="github",
+ user_id=test_user.id,
+ )
+ release = Release(
+ release_id=payload["release"]["id"],
+ tag=event.payload["release"]["tag_name"],
+ repository_id=repo_id,
+ event=event,
+ status=ReleaseStatus.RECEIVED,
+ )
+ # Idea is to test the public interface of GithubRelease
+ rel_api = VCSRelease(release)
+ resolved_url = rel_api.resolve_zipball_url()
+ ref_tag_url = (
+ "https://github.com/auser/repo-2/zipball/refs/tags/v1.0-tag-and-branch"
+ )
+ assert resolved_url == ref_tag_url
+ # Check that the original zipball URL from the event payload is not the same
+ assert rel_api.release_zipball_url != ref_tag_url
+"""
diff --git a/tests/test_tasks.py b/tests/test_tasks.py
index 88ada337..15d857bf 100644
--- a/tests/test_tasks.py
+++ b/tests/test_tasks.py
@@ -1,89 +1,75 @@
# -*- coding: utf-8 -*-
#
-# This file is part of Invenio.
-# Copyright (C) 2023 CERN.
+# Copyright (C) 2025 CERN.
#
-# Invenio is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Invenio is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Invenio. If not, see .
-#
-# In applying this licence, CERN does not waive the privileges and immunities
-# granted to it by virtue of its status as an Intergovernmental Organization
-# or submit itself to any jurisdiction.
+# Invenio-VCS is free software; you can redistribute it and/or modify
+# it under the terms of the MIT License; see LICENSE file for more details.
+"""Test celery task handlers."""
from time import sleep
+from unittest.mock import patch
from invenio_oauthclient.models import RemoteAccount
from invenio_webhooks.models import Event
-from mock import patch
-
-from invenio_github.api import GitHubAPI
-from invenio_github.models import Release, ReleaseStatus, Repository
-from invenio_github.tasks import process_release, refresh_accounts
-from invenio_github.utils import iso_utcnow
-from . import fixtures
+from invenio_vcs.generic_models import (
+ GenericOwner,
+ GenericRelease,
+ GenericRepository,
+)
+from invenio_vcs.models import Release, ReleaseStatus
+from invenio_vcs.providers import RepositoryServiceProvider
+from invenio_vcs.service import VCSService
+from invenio_vcs.tasks import process_release, refresh_accounts
+from invenio_vcs.utils import iso_utcnow
+from tests.contrib_fixtures.patcher import TestProviderPatcher
def test_real_process_release_task(
- app, db, location, tester_id, remote_token, github_api
+ db,
+ tester_id,
+ vcs_service: VCSService,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_release: GenericRelease,
+ test_generic_owner: GenericOwner,
+ provider_patcher: TestProviderPatcher,
):
- # Initialise account
- api = GitHubAPI(tester_id)
- api.init_account()
- api.sync()
+ vcs_service.sync()
- # Get remote account extra data
- extra_data = remote_token.remote_account.extra_data
+ generic_repo = test_generic_repositories[0]
+ vcs_service.enable_repository(generic_repo.id)
+ db_repo = vcs_service.get_repository(generic_repo.id)
- assert 1 in extra_data["repos"]
- assert "repo-1" in extra_data["repos"][1]["full_name"]
- assert 2 in extra_data["repos"]
- assert "repo-2" in extra_data["repos"][2]["full_name"]
-
- repo_name = "repo-1"
- repo_id = 1
-
- repo = Repository.create(tester_id, repo_id, repo_name)
- api.enable_repo(repo, 12345)
event = Event(
- receiver_id="github",
+ # Receiver ID is same as provider ID
+ receiver_id=vcs_service.provider.factory.id,
user_id=tester_id,
- payload=fixtures.PAYLOAD("auser", "repo-1", 1),
+ payload=provider_patcher.test_webhook_payload(
+ generic_repo, test_generic_release, test_generic_owner
+ ),
)
- release_object = Release(
- release_id=event.payload["release"]["id"],
- tag=event.payload["release"]["tag_name"],
- repository=repo,
+ db_release = Release(
+ provider=vcs_service.provider.factory.id,
+ provider_id=test_generic_release.id,
+ tag=test_generic_release.tag_name,
+ repository=db_repo,
event=event,
status=ReleaseStatus.RECEIVED,
)
- db.session.add(release_object)
+ db.session.add(db_release)
db.session.commit()
- process_release.delay(release_object.release_id)
- assert repo.releases.count() == 1
- release = repo.releases.first()
+ process_release.delay(vcs_service.provider.factory.id, db_release.provider_id)
+ assert db_repo.releases.count() == 1
+ release = db_repo.releases.first()
assert release.status == ReleaseStatus.PUBLISHED
- # This uuid is a fake one set by TestGithubRelease fixture
+ # This uuid is a fake one set by TestVCSRelease fixture
assert str(release.record_id) == "445aaacd-9de1-41ab-af52-25ab6cb93df7"
-def test_refresh_accounts(app, db, tester_id, remote_token, github_api):
- """Test account refresh task."""
-
+def test_refresh_accounts(db, test_user, vcs_provider: RepositoryServiceProvider):
def mocked_sync(hooks=True, async_hooks=True):
- """Mock sync function and update the remote account."""
account = RemoteAccount.query.all()[0]
account.extra_data.update(
dict(
@@ -92,15 +78,15 @@ def mocked_sync(hooks=True, async_hooks=True):
)
db.session.commit()
- with patch("invenio_github.api.GitHubAPI.sync", side_effect=mocked_sync):
+ with patch("invenio_vcs.service.VCSService.sync", side_effect=mocked_sync):
updated = RemoteAccount.query.all()[0].updated
expiration_threshold = {"seconds": 1}
sleep(2)
- refresh_accounts.delay(expiration_threshold)
+ refresh_accounts.delay(vcs_provider.factory.id, expiration_threshold)
last_update = RemoteAccount.query.all()[0].updated
assert updated != last_update
- refresh_accounts.delay(expiration_threshold)
+ refresh_accounts.delay(vcs_provider.factory.id, expiration_threshold)
assert last_update == RemoteAccount.query.all()[0].updated
diff --git a/tests/test_views.py b/tests/test_views.py
index 8e447218..5ea770e1 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -2,28 +2,37 @@
#
# Copyright (C) 2023 CERN.
#
-# Invenio-Github is free software; you can redistribute it and/or modify
+# Invenio-VCS is free software; you can redistribute it and/or modify
# it under the terms of the MIT License; see LICENSE file for more details.
-"""Test invenio-github views."""
+"""Test invenio-vcs views."""
+
+from flask import url_for
from flask_security import login_user
from invenio_accounts.testutils import login_user_via_session
-from invenio_oauthclient.models import RemoteAccount
+
+from invenio_vcs.generic_models import GenericRepository
+from invenio_vcs.service import VCSService
-def test_api_init_user(app, client, github_api, test_user):
+def test_api_sync(
+ app,
+ client,
+ test_user,
+ vcs_service: VCSService,
+ test_generic_repositories: list[GenericRepository],
+):
# Login the user
login_user(test_user)
login_user_via_session(client, email=test_user.email)
- # Initialise user account
- res = client.post("/user/github", follow_redirects=True)
+ assert len(list(vcs_service.user_available_repositories)) == 0
+ res = client.post(
+ url_for(
+ "invenio_vcs_api.sync_user_repositories",
+ provider=vcs_service.provider.factory.id,
+ )
+ )
assert res.status_code == 200
-
- # Validate RemoteAccount exists between querying it
- remote_accounts = RemoteAccount.query.filter_by(user_id=test_user.id).all()
- assert len(remote_accounts) == 1
- remote_account = remote_accounts[0]
-
- # Account init adds user's github data to its remote account extra data
- assert remote_account.extra_data
- assert len(remote_account.extra_data.keys())
+ assert len(list(vcs_service.user_available_repositories)) == len(
+ test_generic_repositories
+ )
diff --git a/tests/test_webhook.py b/tests/test_webhook.py
index 14c3317e..2938c326 100644
--- a/tests/test_webhook.py
+++ b/tests/test_webhook.py
@@ -1,58 +1,73 @@
# -*- coding: utf-8 -*-
-#
# This file is part of Invenio.
-# Copyright (C) 2023 CERN.
-#
-# Invenio is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Invenio is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
+# Copyright (C) 2025 CERN.
#
-# You should have received a copy of the GNU General Public License
-# along with Invenio. If not, see .
-#
-# In applying this licence, CERN does not waive the privileges and immunities
-# granted to it by virtue of its status as an Intergovernmental Organization
-# or submit itself to any jurisdiction.
+# Invenio is free software; you can redistribute it and/or modify it
+# under the terms of the MIT License; see LICENSE file for more details.
-"""Test GitHub hook."""
+"""Test vcs hook."""
import json
# from invenio_rdm_records.proxies import current_rdm_records_service
from invenio_webhooks.models import Event
-from invenio_github.api import GitHubAPI
-from invenio_github.models import ReleaseStatus, Repository
-
-
-def test_webhook_post(app, db, tester_id, remote_token, github_api):
- """Test webhook POST success."""
- from . import fixtures
-
- repo_id = 3
- repo_name = "arepo"
- hook = 1234
- tag = "v1.0"
-
- repo = Repository.get(github_id=repo_id, name=repo_name)
- if not repo:
- repo = Repository.create(tester_id, repo_id, repo_name)
+from invenio_vcs.generic_models import (
+ GenericOwner,
+ GenericOwnerType,
+ GenericRelease,
+ GenericRepository,
+ GenericWebhook,
+)
+from invenio_vcs.models import ReleaseStatus, Repository
+from invenio_vcs.utils import utcnow
+from tests.contrib_fixtures.patcher import TestProviderPatcher
+
+
+def test_webhook_post(
+ app,
+ db,
+ tester_id,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_webhooks: list[GenericWebhook],
+ test_generic_release: GenericRelease,
+ test_generic_owner: GenericOwner,
+ provider_patcher: TestProviderPatcher,
+):
+ generic_repo = test_generic_repositories[0]
+ generic_webhook = next(
+ h for h in test_generic_webhooks if h.repository_id == generic_repo.id
+ )
- api = GitHubAPI(tester_id)
+ db_repo = Repository.get(
+ provider=provider_patcher.provider_factory().id, provider_id=generic_repo.id
+ )
+ if not db_repo:
+ db_repo = Repository.create(
+ provider=provider_patcher.provider_factory().id,
+ provider_id=generic_repo.id,
+ default_branch=generic_repo.default_branch,
+ full_name=generic_repo.full_name,
+ description=generic_repo.description,
+ license_spdx=generic_repo.license_spdx,
+ )
# Enable repository webhook.
- api.enable_repo(repo, hook)
-
- payload = json.dumps(fixtures.PAYLOAD("auser", repo_name, repo_id, tag))
+ db_repo.hook = generic_webhook.id
+ db_repo.enabled_by_user_id = tester_id
+ db.session.add(db_repo)
+ db.session.commit()
+
+ payload = json.dumps(
+ provider_patcher.test_webhook_payload(
+ generic_repo, test_generic_release, test_generic_owner
+ )
+ )
headers = [("Content-Type", "application/json")]
with app.test_request_context(headers=headers, data=payload):
- event = Event.create(receiver_id="github", user_id=tester_id)
+ event = Event.create(
+ receiver_id=provider_patcher.provider_factory().id, user_id=tester_id
+ )
# Add event to session. Otherwise defaults are not added (e.g. response and response_code)
db.session.add(event)
db.session.commit()
@@ -60,47 +75,68 @@ def test_webhook_post(app, db, tester_id, remote_token, github_api):
assert event.response_code == 202
# Validate that a release was created
- assert repo.releases.count() == 1
- release = repo.releases.first()
+ assert db_repo.releases.count() == 1
+ release = db_repo.releases.first()
assert release.status == ReleaseStatus.PUBLISHED
- assert release.release_id == event.payload["release"]["id"]
- assert release.tag == tag
- # This uuid is a fake one set by TestGithubRelease fixture
+ assert release.provider_id == test_generic_release.id
+ assert release.tag == test_generic_release.tag_name
+ # This uuid is a fake one set by TestVCSRelease fixture
assert str(release.record_id) == "445aaacd-9de1-41ab-af52-25ab6cb93df7"
assert release.errors is None
-def test_webhook_post_fail(app, tester_id, remote_token, github_api):
- """Test webhook POST failure."""
- from . import fixtures
-
- repo_id = 3
- repo_name = "arepo"
- hook = 1234
-
- # Create a repository
- repo = Repository.get(github_id=repo_id, name=repo_name)
- if not repo:
- repo = Repository.create(tester_id, repo_id, repo_name)
+def test_webhook_post_fail(
+ app,
+ tester_id,
+ test_generic_repositories: list[GenericRepository],
+ test_generic_webhooks: list[GenericWebhook],
+ provider_patcher: TestProviderPatcher,
+):
+ generic_repo = test_generic_repositories[0]
+ generic_webhook = next(
+ h for h in test_generic_webhooks if h.repository_id == generic_repo.id
+ )
- api = GitHubAPI(tester_id)
+ db_repo = Repository.get(
+ provider=provider_patcher.provider_factory().id, provider_id=generic_repo.id
+ )
+ if not db_repo:
+ db_repo = Repository.create(
+ provider=provider_patcher.provider_factory().id,
+ provider_id=generic_repo.id,
+ default_branch=generic_repo.default_branch,
+ full_name=generic_repo.full_name,
+ description=generic_repo.description,
+ license_spdx=generic_repo.license_spdx,
+ )
# Enable repository webhook.
- api.enable_repo(repo, hook)
+ db_repo.hook = generic_webhook.id
+ db_repo.enabled_by_user_id = tester_id
# Create an invalid payload (fake repo)
fake_payload = json.dumps(
- fixtures.PAYLOAD("fake_user", "fake_repo", 1000, "v1000.0")
+ provider_patcher.test_webhook_payload(
+ GenericRepository(
+ id="123",
+ full_name="fake_repo",
+ default_branch="fake_branch",
+ ),
+ GenericRelease(
+ id="123",
+ tag_name="v123.345",
+ created_at=utcnow(),
+ ),
+ GenericOwner(id="123", path_name="fake_user", type=GenericOwnerType.Person),
+ )
)
headers = [("Content-Type", "application/json")]
with app.test_request_context(headers=headers, data=fake_payload):
# user_id = request.oauth.access_token.user_id
- event = Event.create(receiver_id="github", user_id=tester_id)
+ event = Event.create(
+ receiver_id=provider_patcher.provider_factory().id, user_id=tester_id
+ )
event.process()
# Repo does not exist
assert event.response_code == 404
-
- # Create an invalid payload (fake user)
- # TODO 'fake_user' does not match the invenio user 'extra_data'. Should this fail?
- # TODO what should happen if an event is received and the account is not synced?