Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add update repository mutation #573

Merged
merged 7 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest
from asgiref.sync import async_to_sync
from django.contrib.auth.models import AnonymousUser
from django.test import TransactionTestCase

from codecov.commands.exceptions import Unauthorized
from codecov_auth.tests.factories import OwnerFactory
from core.tests.factories import RepositoryFactory, RepositoryTokenFactory

from ..update_repository import UpdateRepositoryInteractor


class UpdateRepositoryInteractorTest(TransactionTestCase):
def setUp(self):
self.owner = OwnerFactory(username="codecov")
self.random_user = OwnerFactory(organizations=[])

def execute_unauthorized_owner(self):
return UpdateRepositoryInteractor(self.owner, "github").execute(
repo_name="repo-1",
owner=self.random_user,
default_branch=None,
activated=None,
)

async def test_when_validation_error_unauthorized_owner(self):
with pytest.raises(Unauthorized):
await self.execute_unauthorized_owner()
45 changes: 45 additions & 0 deletions core/commands/repository/interactors/update_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Optional

from django.conf import settings

import services.self_hosted as self_hosted
from codecov.commands.base import BaseInteractor
from codecov.commands.exceptions import Unauthenticated, Unauthorized, ValidationError
from codecov.db import sync_to_async
from codecov_auth.helpers import current_user_part_of_org
from codecov_auth.models import Owner
from core.models import Repository


class UpdateRepositoryInteractor(BaseInteractor):
def validate_owner(self, owner: Owner):
if not self.current_user.is_authenticated:
raise Unauthenticated()

if not current_user_part_of_org(self.current_owner, owner):
raise Unauthorized()

@sync_to_async
def execute(
self,
repo_name: str,
owner: Owner,
default_branch: Optional[str],
activated: Optional[bool],
):
self.validate_owner(owner)
repo = Repository.objects.filter(author_id=owner.pk, name=repo_name).first()
if not repo:
raise ValidationError("Repo not found")

if default_branch:
branch = repo.branches.filter(name=default_branch).first()
if branch is None:
raise ValidationError(
f"The branch '{default_branch}' is not in our records. Please provide a valid branch name.",
)

repo.branch = default_branch
if activated:
repo.activated = activated
repo.save()
14 changes: 14 additions & 0 deletions core/commands/repository/repository.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Optional

from codecov.commands.base import BaseCommand
from codecov_auth.models import Owner
from core.models import Repository
Expand All @@ -9,12 +11,24 @@
from .interactors.get_repository_token import GetRepositoryTokenInteractor
from .interactors.get_upload_token import GetUploadTokenInteractor
from .interactors.regenerate_repository_token import RegenerateRepositoryTokenInteractor
from .interactors.update_repository import UpdateRepositoryInteractor


class RepositoryCommands(BaseCommand):
def fetch_repository(self, owner, name):
return self.get_interactor(FetchRepositoryInteractor).execute(owner, name)

def update_repository(
self,
repo_name: str,
owner: Owner,
default_branch: Optional[str],
activated: Optional[bool],
):
return self.get_interactor(UpdateRepositoryInteractor).execute(
repo_name, owner, default_branch, activated
)

def get_upload_token(self, repository):
return self.get_interactor(GetUploadTokenInteractor).execute(repository)

Expand Down
106 changes: 106 additions & 0 deletions graphql_api/tests/mutation/test_update_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from django.test import TransactionTestCase, override_settings

from codecov_auth.tests.factories import OwnerFactory
from core.tests.factories import BranchFactory, RepositoryFactory
from graphql_api.tests.helper import GraphQLTestHelper

query = """
mutation($input: UpdateRepositoryInput!) {
updateRepository(input: $input) {
error {
__typename
... on ResolverError {
message
}
}
}
}
"""

repo_query = """{
me {
owner {
repository(name: "gazebo") {
... on Repository {
activated
defaultBranch
}
}
}
}
}
"""


class UpdateRepositoryTests(GraphQLTestHelper, TransactionTestCase):
def setUp(self):
self.org = OwnerFactory(username="codecov", service="github")
self.repo = RepositoryFactory(author=self.org, name="gazebo", activated=False)

def test_when_authenticated_update_activated(self):
data = self.gql_request(
query,
owner=self.org,
variables={"input": {"activated": True, "repoName": "gazebo"}},
)

repo_result = self.gql_request(
repo_query,
owner=self.org,
)
assert repo_result["me"]["owner"]["repository"]["activated"] == True

assert data == {"updateRepository": None}

def test_when_authenticated_update_branch(self):
other_branch = BranchFactory.create(
name="some other branch", repository=self.repo
)
data = self.gql_request(
query,
owner=self.org,
variables={"input": {"branch": "some other branch", "repoName": "gazebo"}},
)

repo_result = self.gql_request(
repo_query,
owner=self.org,
)
assert (
repo_result["me"]["owner"]["repository"]["defaultBranch"]
== "some other branch"
)

assert data == {"updateRepository": None}

def test_when_authenticated_branch_does_not_exist(self):
data = self.gql_request(
query,
owner=self.org,
variables={"input": {"branch": "Dne", "repoName": "gazebo"}},
)

assert data["updateRepository"]["error"]["__typename"] == "ValidationError"

def test_when_unauthenticated(self):
data = self.gql_request(
query,
variables={
"input": {
"repoName": "gazebo",
}
},
)
assert data["updateRepository"]["error"]["__typename"] == "UnauthenticatedError"

def test_when_validation_error_repo_not_found(self):
data = self.gql_request(
query,
owner=self.org,
variables={
"input": {
"repoName": "DNE",
}
},
)
assert data["updateRepository"]["error"]["__typename"] == "ValidationError"
2 changes: 2 additions & 0 deletions graphql_api/types/mutation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .sync_with_git_provider import gql_sync_with_git_provider
from .update_default_organization import gql_update_default_organization
from .update_profile import gql_update_profile
from .update_repository import gql_update_repository
from .update_self_hosted_settings import gql_update_self_hosted_settings

mutation = ariadne_load_local_graphql(__file__, "mutation.graphql")
Expand All @@ -40,4 +41,5 @@
mutation = mutation + gql_start_trial
mutation = mutation + gql_cancel_trial
mutation = mutation + gql_delete_component_measurements
mutation = mutation + gql_update_repository
mutation = mutation + gql_update_self_hosted_settings
1 change: 1 addition & 0 deletions graphql_api/types/mutation/mutation.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ type Mutation {
saveSentryState(input: SaveSentryStateInput!): SaveSentryStatePayload
saveTermsAgreement(input: SaveTermsAgreementInput!): SaveTermsAgreementPayload
deleteComponentMeasurements(input: DeleteComponentMeasurementsInput!): DeleteComponentMeasurementsPayload
updateRepository(input: UpdateRepositoryInput!): UpdateRepositoryPayload
updateSelfHostedSettings(input: UpdateSelfHostedSettingsInput!): UpdateSelfHostedSettingsPayload
}
3 changes: 3 additions & 0 deletions graphql_api/types/mutation/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
resolve_update_default_organization,
)
from .update_profile import error_update_profile, resolve_update_profile
from .update_repository import error_update_repository, resolve_update_repository
from .update_self_hosted_settings import (
error_update_self_hosted_settings,
resolve_update_self_hosted_settings,
Expand Down Expand Up @@ -71,6 +72,7 @@
mutation_bindable.field("deleteComponentMeasurements")(
resolve_delete_component_measurements
)
mutation_bindable.field("updateRepository")(resolve_update_repository)
mutation_bindable.field("updateSelfHostedSettings")(resolve_update_self_hosted_settings)


Expand All @@ -94,5 +96,6 @@
error_save_terms_agreement,
error_start_trial,
error_cancel_trial,
error_update_repository,
error_update_self_hosted_settings,
]
7 changes: 7 additions & 0 deletions graphql_api/types/mutation/update_repository/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from graphql_api.helpers.ariadne import ariadne_load_local_graphql

from .update_repository import error_update_repository, resolve_update_repository

gql_update_repository = ariadne_load_local_graphql(
__file__, "update_repository.graphql"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
union UpdateRepositoryError = UnauthenticatedError | ValidationError | UnauthorizedError

type UpdateRepositoryPayload {
error: UpdateRepositoryError
}

input UpdateRepositoryInput {
branch: String
activated: Boolean
repoName: String!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from ariadne import UnionType

from graphql_api.helpers.mutation import (
resolve_union_error_type,
wrap_error_handling_mutation,
)


@wrap_error_handling_mutation
async def resolve_update_repository(_, info, input):
command = info.context["executor"].get_command("repository")
owner = info.context["request"].current_owner
repo_name = input.get("repoName")
default_branch = input.get("branch")
activated = input.get("activated")
await command.update_repository(
repo_name,
owner,
default_branch,
activated,
)


error_update_repository = UnionType("UpdateRepositoryError")
error_update_repository.type_resolver(resolve_union_error_type)
Loading