From d34e6ad12406c66e544fa035a8bbdc6835f3b5b3 Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Wed, 8 Oct 2025 16:22:31 +0200 Subject: [PATCH 1/4] tests: compatibility for invenio-vcs * Rewrote unit tests for compatibility with new invenio-vcs package by making the tests provider-agnostic. This involved creating patches to emulate the low-level behaviour of provider SDKs (for GitHub and GitLab) based on an abstract interface. Future provider implementations will also need corresponding test mock implementations. * Generally renamed many references in the tests from invenio-github to invenio-vcs. They should all currently be passing, but it's only possible to fully test them if you have all the PRs for the new module assembled. This is already done on the `master` branch of https://github.com/palkerecsenyi/invenio-vcs for convenience. * This commit on its own is UNRELEASABLE. We will merge multiple commits related to the VCS upgrade into the `vcs-staging` branch and then merge them all into `master` once we have a fully release-ready prototype. At that point, we will create a squash commit. --- setup.cfg | 1 - tests/conftest.py | 334 ++++--- tests/contrib_fixtures/github.py | 813 ++++++++++++++++++ tests/contrib_fixtures/gitlab.py | 393 +++++++++ tests/contrib_fixtures/patcher.py | 67 ++ tests/fixtures.py | 459 +--------- tests/test_alembic.py | 5 +- tests/test_api.py | 119 --- tests/test_badge.py | 103 ++- ..._invenio_github.py => test_invenio_vcs.py} | 14 +- tests/test_models.py | 13 +- tests/test_provider.py | 92 ++ tests/test_service.py | 177 ++++ tests/test_tasks.py | 82 +- tests/test_views.py | 34 +- tests/test_webhook.py | 145 +++- 16 files changed, 2017 insertions(+), 834 deletions(-) create mode 100644 tests/contrib_fixtures/github.py create mode 100644 tests/contrib_fixtures/gitlab.py create mode 100644 tests/contrib_fixtures/patcher.py delete mode 100644 tests/test_api.py rename tests/{test_invenio_github.py => test_invenio_vcs.py} (81%) create mode 100644 tests/test_provider.py create mode 100644 tests/test_service.py 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..108123f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,28 +28,48 @@ 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 +83,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 +121,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,7 +151,7 @@ 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. @@ -142,33 +162,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 +206,157 @@ 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", + html_url="https://example.com/example/example1", + description="Lorem ipsum", + license_spdx="MIT", + ), + GenericRepository( + id="2", + full_name="repo-2", + default_branch="main", + html_url="https://example.com/example/example2", + description="Lorem ipsum", + license_spdx="MIT", + ), + GenericRepository( + id="3", + full_name="repo-3", + default_branch="main", + html_url="https://example.com/example/example3", + description="Lorem ipsum", + license_spdx="MIT", + ), + ] @pytest.fixture() -def test_repo_data_two(): - """Test repository.""" - return {"name": "repo-2", "id": 2} +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_repo_data_three(): - """Test repository.""" - return {"name": "arepo", "id": 3} +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 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_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"), + ] - repo_1 = github3.repos.Repository( - github_repo_metadata( - "auser", test_repo_data_one["name"], test_repo_data_one["id"] - ), - mock_api.session, + +@pytest.fixture() +def test_generic_user(): + """Provider-common user to own the repositories.""" + return GenericUser(id="1", username="user1", display_name="Test User") + + +@pytest.fixture() +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.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(), + html_url="https://example.com/v1.0", + 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..420374ed --- /dev/null +++ b/tests/contrib_fixtures/gitlab.py @@ -0,0 +1,393 @@ +# -*- 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 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..9e481f26 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -21,17 +21,11 @@ # or submit itself to any jurisdiction. """Define fixtures for tests.""" -import os -from base64 import b64encode -from zipfile import ZipFile +from invenio_vcs.models import ReleaseStatus +from invenio_vcs.service import VCSRelease -from six import BytesIO -from invenio_github.api import GitHubRelease -from invenio_github.models import ReleaseStatus - - -class TestGithubRelease(GitHubRelease): +class TestVCSRelease(VCSRelease): """Implements GithubRelease with test methods.""" def publish(self): @@ -39,8 +33,8 @@ def publish(self): 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 +49,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..cae48f99 100644 --- a/tests/test_alembic.py +++ b/tests/test_alembic.py @@ -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..e8fe6a77 100644 --- a/tests/test_badge.py +++ b/tests/test_badge.py @@ -24,20 +24,93 @@ 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_vcs.py similarity index 81% rename from tests/test_invenio_github.py rename to tests/test_invenio_vcs.py index ceed134a..c75a4daa 100644 --- a/tests/test_invenio_github.py +++ b/tests/test_invenio_vcs.py @@ -27,12 +27,12 @@ from flask import Flask -from invenio_github import InvenioGitHub +from invenio_vcs import InvenioVCS def test_version(): """Test version import.""" - from invenio_github import __version__ + from invenio_vcs import __version__ assert __version__ @@ -40,11 +40,11 @@ def test_version(): def test_init(): """Test extension initialization.""" app = Flask("testapp") - ext = InvenioGitHub(app) - assert "invenio-github" in app.extensions + ext = InvenioVCS(app) + assert "invenio-vcs" in app.extensions app = Flask("testapp") - ext = InvenioGitHub() - assert "invenio-github" not in app.extensions + ext = InvenioVCS() + assert "invenio-vcs" not in app.extensions ext.init_app(app) - assert "invenio-github" in app.extensions + assert "invenio-vcs" in app.extensions diff --git a/tests/test_models.py b/tests/test_models.py index be63e40f..00354b78 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -20,11 +20,16 @@ # granted to it by virtue of its status as an Intergovernmental Organization # or submit itself to any jurisdiction. -"""Test cases for badge creation.""" +"""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..0087ff3d --- /dev/null +++ b/tests/test_provider.py @@ -0,0 +1,92 @@ +# -*- 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. +"""Test invenio-vcs providers.""" + +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..a8f8997c --- /dev/null +++ b/tests/test_service.py @@ -0,0 +1,177 @@ +# -*- 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_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(repo_id=test_generic_repositories[0].id) + assert repository is not None + assert repository.provider_id == test_generic_repositories[0].id + + +# GithubRelease api tests + + +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 GithubRelease + 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..6cb659b9 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -21,69 +21,69 @@ # or submit itself to any jurisdiction. 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() - - # Get remote account extra data - extra_data = remote_token.remote_account.extra_data + vcs_service.sync() - 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"] + generic_repo = test_generic_repositories[0] + vcs_service.enable_repository(generic_repo.id) + db_repo = vcs_service.get_repository(repo_id=generic_repo.id) - 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 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 +92,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..38a099ad 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -5,25 +5,33 @@ # 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 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..c847e4d7 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -27,32 +27,63 @@ # 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, + html_url=generic_repo.html_url, + 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 +91,71 @@ 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 + assert release.provider_id == test_generic_release.id + assert release.tag == test_generic_release.tag_name # This uuid is a fake one set by TestGithubRelease 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, + html_url=generic_repo.html_url, + 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", + html_url="https://example.com", + ), + GenericRelease( + id="123", + tag_name="v123.345", + created_at=utcnow(), + html_url="https://example.com", + ), + 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? From 9731f0afb7ec05e91b2efd258607c78fa244a85e Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 9 Oct 2025 17:55:07 +0200 Subject: [PATCH 2/4] tests: rename more comments for consistency * Removed references to Invenio-GitHub and replaced with "VCS" where applicable --- tests/conftest.py | 25 +++---------------------- tests/contrib_fixtures/gitlab.py | 5 ++++- tests/fixtures.py | 25 +++++-------------------- tests/test_alembic.py | 4 ++-- tests/test_badge.py | 22 +++------------------- tests/test_invenio_vcs.py | 25 +++---------------------- tests/test_models.py | 22 +++------------------- tests/test_provider.py | 4 ++-- tests/test_service.py | 9 +++------ tests/test_tasks.py | 24 +++++------------------- tests/test_views.py | 5 +++-- tests/test_webhook.py | 4 ++-- 12 files changed, 38 insertions(+), 136 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 108123f5..24a9c058 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,28 +1,9 @@ # -*- 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 @@ -154,7 +135,7 @@ def running_app(app, location, cache): 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( diff --git a/tests/contrib_fixtures/gitlab.py b/tests/contrib_fixtures/gitlab.py index 420374ed..b4282c06 100644 --- a/tests/contrib_fixtures/gitlab.py +++ b/tests/contrib_fixtures/gitlab.py @@ -2,8 +2,11 @@ # # Copyright (C) 2025 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. +# +# 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 diff --git a/tests/fixtures.py b/tests/fixtures.py index 9e481f26..ff41d7dc 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,32 +1,17 @@ # -*- 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.""" + from invenio_vcs.models import ReleaseStatus from invenio_vcs.service import VCSRelease class TestVCSRelease(VCSRelease): - """Implements GithubRelease with test methods.""" + """Implements VCSRelease with test methods.""" def publish(self): """Sets release status to published. diff --git a/tests/test_alembic.py b/tests/test_alembic.py index cae48f99..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 diff --git a/tests/test_badge.py b/tests/test_badge.py index e8fe6a77..76d42d94 100644 --- a/tests/test_badge.py +++ b/tests/test_badge.py @@ -1,25 +1,9 @@ # -*- 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 diff --git a/tests/test_invenio_vcs.py b/tests/test_invenio_vcs.py index c75a4daa..af033139 100644 --- a/tests/test_invenio_vcs.py +++ b/tests/test_invenio_vcs.py @@ -1,28 +1,9 @@ # -*- 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 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. """Module tests.""" from flask import Flask diff --git a/tests/test_models.py b/tests/test_models.py index 00354b78..241b139f 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,25 +1,9 @@ # -*- 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. """Test cases for VCS models.""" from invenio_vcs.models import Repository diff --git a/tests/test_provider.py b/tests/test_provider.py index 0087ff3d..ee3343c5 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -2,9 +2,9 @@ # # Copyright (C) 2025 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-vcs providers.""" +"""Test invenio-vcs provider layer.""" from invenio_vcs.generic_models import ( GenericContributor, diff --git a/tests/test_service.py b/tests/test_service.py index a8f8997c..0d47f489 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -2,9 +2,9 @@ # # Copyright (C) 2023-2025 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 api.""" +"""Test invenio-vcs service layer.""" import json @@ -79,9 +79,6 @@ def test_vcs_service_get_repository( assert repository.provider_id == test_generic_repositories[0].id -# GithubRelease api tests - - def test_release_api( app, test_user, @@ -110,7 +107,7 @@ def test_release_api( status=ReleaseStatus.RECEIVED, ) - # Idea is to test the public interface of GithubRelease + # Idea is to test the public interface of VCSRelease r = VCSRelease(release, vcs_service.provider) # Validate that public methods raise NotImplementedError diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 6cb659b9..3e89843c 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -1,24 +1,10 @@ # -*- 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 @@ -78,7 +64,7 @@ def test_real_process_release_task( 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" diff --git a/tests/test_views.py b/tests/test_views.py index 38a099ad..5ea770e1 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -2,9 +2,10 @@ # # 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 diff --git a/tests/test_webhook.py b/tests/test_webhook.py index c847e4d7..92fd5ad7 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -20,7 +20,7 @@ # granted to it by virtue of its status as an Intergovernmental Organization # or submit itself to any jurisdiction. -"""Test GitHub hook.""" +"""Test vcs hook.""" import json @@ -96,7 +96,7 @@ def test_webhook_post( assert release.status == ReleaseStatus.PUBLISHED assert release.provider_id == test_generic_release.id assert release.tag == test_generic_release.tag_name - # 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" assert release.errors is None From 1dc5f506a38f0a4a47a053f332f44f8eb534fd8b Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Wed, 15 Oct 2025 15:37:12 +0200 Subject: [PATCH 3/4] WIP: chore: license --- tests/test_webhook.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 92fd5ad7..29552d0b 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -1,24 +1,9 @@ # -*- 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. -# -# You should have received a copy of the GNU General Public License -# along with Invenio. If not, see . +# Copyright (C) 2025 CERN. # -# 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 vcs hook.""" From a091adf9af1527a2987438773c0285159ed7529c Mon Sep 17 00:00:00 2001 From: Pal Kerecsenyi Date: Thu, 23 Oct 2025 11:31:39 +0200 Subject: [PATCH 4/4] WIP: remove html_url --- tests/conftest.py | 4 ---- tests/test_webhook.py | 4 ---- 2 files changed, 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 24a9c058..1129eb45 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -194,7 +194,6 @@ def test_generic_repositories(): id="1", full_name="repo-1", default_branch="main", - html_url="https://example.com/example/example1", description="Lorem ipsum", license_spdx="MIT", ), @@ -202,7 +201,6 @@ def test_generic_repositories(): id="2", full_name="repo-2", default_branch="main", - html_url="https://example.com/example/example2", description="Lorem ipsum", license_spdx="MIT", ), @@ -210,7 +208,6 @@ def test_generic_repositories(): id="3", full_name="repo-3", default_branch="main", - html_url="https://example.com/example/example3", description="Lorem ipsum", license_spdx="MIT", ), @@ -275,7 +272,6 @@ def test_generic_release(): id="1", tag_name="v1.0", created_at=utcnow(), - html_url="https://example.com/v1.0", name="Example release", body="Lorem ipsum dolor sit amet", published_at=utcnow(), diff --git a/tests/test_webhook.py b/tests/test_webhook.py index 29552d0b..2938c326 100644 --- a/tests/test_webhook.py +++ b/tests/test_webhook.py @@ -46,7 +46,6 @@ def test_webhook_post( db_repo = Repository.create( provider=provider_patcher.provider_factory().id, provider_id=generic_repo.id, - html_url=generic_repo.html_url, default_branch=generic_repo.default_branch, full_name=generic_repo.full_name, description=generic_repo.description, @@ -105,7 +104,6 @@ def test_webhook_post_fail( db_repo = Repository.create( provider=provider_patcher.provider_factory().id, provider_id=generic_repo.id, - html_url=generic_repo.html_url, default_branch=generic_repo.default_branch, full_name=generic_repo.full_name, description=generic_repo.description, @@ -123,13 +121,11 @@ def test_webhook_post_fail( id="123", full_name="fake_repo", default_branch="fake_branch", - html_url="https://example.com", ), GenericRelease( id="123", tag_name="v123.345", created_at=utcnow(), - html_url="https://example.com", ), GenericOwner(id="123", path_name="fake_user", type=GenericOwnerType.Person), )