Skip to content

Commit

Permalink
Add update self hosted settings mutation (#559)
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitvinnakota-codecov authored May 22, 2024
1 parent e1af712 commit ad629f2
Show file tree
Hide file tree
Showing 13 changed files with 252 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from unittest.mock import patch

import pytest
from asgiref.sync import async_to_sync
from django.contrib.auth.models import AnonymousUser
from django.test import TransactionTestCase, override_settings

from codecov.commands.exceptions import Unauthenticated, ValidationError
from codecov_auth.commands.owner.interactors.update_self_hosted_settings import (
UpdateSelfHostedSettingsInteractor,
)
from codecov_auth.tests.factories import OwnerFactory


class UpdateSelfHostedSettingsInteractorTest(TransactionTestCase):
@async_to_sync
def execute(
self,
current_user,
input={
"shouldAutoActivate": None,
},
):
return UpdateSelfHostedSettingsInteractor(None, "github", current_user).execute(
input=input,
)

@override_settings(IS_ENTERPRISE=True)
def test_update_self_hosted_settings_when_auto_activate_is_true(self):
owner = OwnerFactory(plan_auto_activate=False)
self.execute(current_user=owner, input={"shouldAutoActivate": True})
owner.refresh_from_db()
assert owner.plan_auto_activate == True

@override_settings(IS_ENTERPRISE=True)
def test_update_self_hosted_settings_when_auto_activate_is_false(self):
owner = OwnerFactory(plan_auto_activate=True)
self.execute(current_user=owner, input={"shouldAutoActivate": False})
owner.refresh_from_db()
assert owner.plan_auto_activate == False

@override_settings(IS_ENTERPRISE=False)
def test_validation_error_when_not_self_hosted_instance(self):
owner = OwnerFactory(plan_auto_activate=True)
with pytest.raises(ValidationError):
self.execute(
current_user=owner,
input={
"shouldAutoActivate": False,
},
)

@override_settings(IS_ENTERPRISE=True)
def test_user_is_not_authenticated(self):
with pytest.raises(Unauthenticated) as e:
self.execute(
current_user=AnonymousUser(),
input={
"shouldAutoActivate": False,
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from dataclasses import dataclass

from django.conf import settings

import services.self_hosted as self_hosted
from codecov.commands.base import BaseInteractor
from codecov.commands.exceptions import Unauthenticated, ValidationError
from codecov.db import sync_to_async
from services.refresh import RefreshService


@dataclass
class UpdateSelfHostedSettingsInput:
auto_activate_members: bool = False


class UpdateSelfHostedSettingsInteractor(BaseInteractor):
def validate(self) -> None:
if not self.current_user.is_authenticated:
raise Unauthenticated()

if not settings.IS_ENTERPRISE:
raise ValidationError(
"enable_autoactivation and disable_autoactivation are only available in self-hosted environments"
)

@sync_to_async
def execute(self, input: UpdateSelfHostedSettingsInput) -> None:
self.validate()
typed_input = UpdateSelfHostedSettingsInput(
auto_activate_members=input.get("shouldAutoActivate"),
)

should_auto_activate = typed_input.auto_activate_members
if should_auto_activate:
self_hosted.enable_autoactivation()
else:
self_hosted.disable_autoactivation()
4 changes: 4 additions & 0 deletions codecov_auth/commands/owner/owner.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .interactors.trigger_sync import TriggerSyncInteractor
from .interactors.update_default_organization import UpdateDefaultOrganizationInteractor
from .interactors.update_profile import UpdateProfileInteractor
from .interactors.update_self_hosted_settings import UpdateSelfHostedSettingsInteractor


class OwnerCommands(BaseCommand):
Expand Down Expand Up @@ -82,3 +83,6 @@ def cancel_trial(self, org_username: str) -> None:
return self.get_interactor(CancelTrialInteractor).execute(
org_username=org_username
)

def update_self_hosted_settings(self, input) -> None:
return self.get_interactor(UpdateSelfHostedSettingsInteractor).execute(input)
70 changes: 70 additions & 0 deletions graphql_api/tests/mutation/test_update_self_hosted_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import pytest
from django.test import TransactionTestCase, override_settings

from codecov.commands.exceptions import ValidationError
from codecov_auth.tests.factories import OwnerFactory
from graphql_api.tests.helper import GraphQLTestHelper

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


class UpdateSelfHostedSettingsTest(GraphQLTestHelper, TransactionTestCase):
def _request(self, owner=None):
return self.gql_request(
query,
variables={"input": {"shouldAutoActivate": True}},
owner=owner,
)

def _request_deactivate(self, owner=None):
return self.gql_request(
query,
variables={"input": {"shouldAutoActivate": False}},
owner=owner,
)

@override_settings(IS_ENTERPRISE=True)
def test_unauthenticated(self):
assert self._request() == {
"updateSelfHostedSettings": {
"error": {
"__typename": "UnauthenticatedError",
"message": "You are not authenticated",
}
}
}

@override_settings(IS_ENTERPRISE=True)
def test_authenticated_enable_autoactivation(self):
owner = OwnerFactory()
assert self._request(owner=owner) == {"updateSelfHostedSettings": None}

@override_settings(IS_ENTERPRISE=True)
def test_authenticate_disable_autoactivation(self):
owner = OwnerFactory()
assert self._request_deactivate(owner=owner) == {
"updateSelfHostedSettings": None
}

@override_settings(IS_ENTERPRISE=False)
def test_invalid_settings(self):
owner = OwnerFactory()
assert self._request(owner=owner) == {
"updateSelfHostedSettings": {
"error": {
"__typename": "ValidationError",
"message": "enable_autoactivation and disable_autoactivation are only available in self-hosted environments",
}
}
}
20 changes: 20 additions & 0 deletions graphql_api/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ def test_seats_used_self_hosted(self, activated_owners):
},
}

def test_plan_auto_activate(self):
data = self.gql_request("query { config { planAutoActivate }}")
assert data == {
"config": {
"planAutoActivate": None,
},
}

@override_settings(IS_ENTERPRISE=True)
@patch("services.self_hosted.is_autoactivation_enabled")
def test_plan_auto_activate_self_hosted(self, is_autoactivation_enabled):
is_autoactivation_enabled.return_value = True

data = self.gql_request("query { config { planAutoActivate }}")
assert data == {
"config": {
"planAutoActivate": True,
},
}

def test_seats_limit(self):
data = self.gql_request("query { config { seatsLimit }}")
assert data == {
Expand Down
1 change: 1 addition & 0 deletions graphql_api/types/config/config.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
type Config {
loginProviders: [LoginProvider!]!
planAutoActivate: Boolean
seatsUsed: Int
seatsLimit: Int
isTimescaleEnabled: Boolean!
Expand Down
11 changes: 10 additions & 1 deletion graphql_api/types/config/config.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import List
from typing import List, Optional

from ariadne import ObjectType
from distutils.util import strtobool
from django.conf import settings
from graphql.type.definition import GraphQLResolveInfo

import services.self_hosted as self_hosted
from codecov.db import sync_to_async
Expand Down Expand Up @@ -65,6 +66,14 @@ def resolve_sync_providers(_, info) -> List[str]:
return sync_providers


@config_bindable.field("planAutoActivate")
def resolve_plan_auto_activate(_, info: GraphQLResolveInfo) -> Optional[bool]:
if not settings.IS_ENTERPRISE:
return None

return self_hosted.is_autoactivation_enabled()


@config_bindable.field("seatsUsed")
@sync_to_async
def resolve_seats_used(_, info):
Expand Down
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_self_hosted_settings import gql_update_self_hosted_settings

mutation = ariadne_load_local_graphql(__file__, "mutation.graphql")
mutation = mutation + gql_create_api_token
Expand All @@ -39,3 +40,4 @@
mutation = mutation + gql_start_trial
mutation = mutation + gql_cancel_trial
mutation = mutation + gql_delete_component_measurements
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,4 +25,5 @@ type Mutation {
saveSentryState(input: SaveSentryStateInput!): SaveSentryStatePayload
saveTermsAgreement(input: SaveTermsAgreementInput!): SaveTermsAgreementPayload
deleteComponentMeasurements(input: DeleteComponentMeasurementsInput!): DeleteComponentMeasurementsPayload
updateSelfHostedSettings(input: UpdateSelfHostedSettingsInput!): UpdateSelfHostedSettingsPayload
}
7 changes: 7 additions & 0 deletions graphql_api/types/mutation/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
resolve_update_default_organization,
)
from .update_profile import error_update_profile, resolve_update_profile
from .update_self_hosted_settings import (
error_update_self_hosted_settings,
resolve_update_self_hosted_settings,
)

mutation_bindable = MutationType()

Expand Down Expand Up @@ -67,6 +71,8 @@
mutation_bindable.field("deleteComponentMeasurements")(
resolve_delete_component_measurements
)
mutation_bindable.field("updateSelfHostedSettings")(resolve_update_self_hosted_settings)


mutation_resolvers = [
mutation_bindable,
Expand All @@ -88,4 +94,5 @@
error_save_terms_agreement,
error_start_trial,
error_cancel_trial,
error_update_self_hosted_settings,
]
10 changes: 10 additions & 0 deletions graphql_api/types/mutation/update_self_hosted_settings/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from graphql_api.helpers.ariadne import ariadne_load_local_graphql

from .update_self_hosted_settings import (
error_update_self_hosted_settings,
resolve_update_self_hosted_settings,
)

gql_update_self_hosted_settings = ariadne_load_local_graphql(
__file__, "update_self_hosted_settings.graphql"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
union UpdateSelfHostedSettingsError = UnauthenticatedError | ValidationError

type UpdateSelfHostedSettingsPayload {
error: UpdateSelfHostedSettingsError
}

input UpdateSelfHostedSettingsInput {
shouldAutoActivate: Boolean!
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from ariadne import UnionType

from codecov_auth.commands.owner import OwnerCommands
from graphql_api.helpers.mutation import (
require_authenticated,
resolve_union_error_type,
wrap_error_handling_mutation,
)


@wrap_error_handling_mutation
@require_authenticated
async def resolve_update_self_hosted_settings(_, info, input):
command: OwnerCommands = info.context["executor"].get_command("owner")
return await command.update_self_hosted_settings(input)


error_update_self_hosted_settings = UnionType("UpdateSelfHostedSettingsError")
error_update_self_hosted_settings.type_resolver(resolve_union_error_type)

0 comments on commit ad629f2

Please sign in to comment.