-
Notifications
You must be signed in to change notification settings - Fork 29
Add support for saving comments in global YAML file #895
Changes from 5 commits
f6aa70c
b29155b
ba7e350
c920eac
4187f39
1d85bc8
909233d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import html | ||
| from typing import Optional | ||
|
|
||
| import yaml | ||
| from shared.validation.exceptions import InvalidYamlException | ||
|
|
@@ -17,21 +18,21 @@ | |
|
|
||
|
|
||
| class SetYamlOnOwnerInteractor(BaseInteractor): | ||
| def validate(self): | ||
| def validate(self) -> None: | ||
| if not self.current_user.is_authenticated: | ||
| raise Unauthenticated() | ||
|
|
||
| def authorize(self): | ||
| def authorize(self) -> None: | ||
| if not current_user_part_of_org(self.current_owner, self.owner): | ||
| raise Unauthorized() | ||
|
|
||
| def get_owner(self, username): | ||
| def get_owner(self, username: str) -> Owner: | ||
| try: | ||
| return Owner.objects.get(username=username, service=self.service) | ||
| except Owner.DoesNotExist: | ||
| raise NotFound() | ||
|
|
||
| def convert_yaml_to_dict(self, yaml_input): | ||
| def convert_yaml_to_dict(self, yaml_input: str) -> Optional[dict]: | ||
| yaml_safe = html.escape(yaml_input, quote=False) | ||
| try: | ||
| yaml_dict = yaml.safe_load(yaml_safe) | ||
|
|
@@ -49,10 +50,12 @@ def convert_yaml_to_dict(self, yaml_input): | |
| raise ValidationError(message) | ||
|
|
||
| @sync_to_async | ||
| def execute(self, username, yaml_input): | ||
| def execute(self, username: str, yaml_input: str) -> Owner: | ||
| self.validate() | ||
| self.owner = self.get_owner(username) | ||
| self.authorize() | ||
| self.owner.yaml = self.convert_yaml_to_dict(yaml_input) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One of two of the actual changes. |
||
| if self.owner.yaml: | ||
| self.owner.yaml["to_string"] = yaml_input | ||
|
||
| self.owner.save() | ||
| return self.owner | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -69,15 +69,25 @@ async def test_user_is_part_of_org_and_yaml_is_good(self): | |
| ) | ||
| # check the interactor returns the right owner | ||
| assert owner_updated.ownerid == self.org.ownerid | ||
| assert owner_updated.yaml == {"codecov": {"require_ci_to_pass": True}} | ||
| assert owner_updated.yaml == { | ||
| "codecov": { | ||
| "require_ci_to_pass": True, | ||
| }, | ||
| "to_string": "\n" "codecov:\n" " require_ci_to_pass: yes\n", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add an example after line 36 in this file where the |
||
| } | ||
|
|
||
| async def test_user_is_part_of_org_and_yaml_has_quotes(self): | ||
| owner_updated = await self.execute( | ||
| self.current_owner, self.org.username, good_yaml_with_quotes | ||
| ) | ||
| # check the interactor returns the right owner | ||
| assert owner_updated.ownerid == self.org.ownerid | ||
| assert owner_updated.yaml == {"codecov": {"bot": "codecov"}} | ||
| assert owner_updated.yaml == { | ||
| "codecov": { | ||
| "bot": "codecov", | ||
| }, | ||
| "to_string": "\n" "codecov:\n" " bot: 'codecov'\n", | ||
| } | ||
|
|
||
| async def test_user_is_part_of_org_and_yaml_is_empty(self): | ||
| owner_updated = await self.execute(self.current_owner, self.org.username, "") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| from datetime import datetime | ||
| from hashlib import sha1 | ||
| from typing import Iterable, List, Optional | ||
| from typing import Any, Iterable, List, Optional | ||
|
|
||
| import shared.rate_limits as rate_limits | ||
| import stripe | ||
|
|
@@ -26,6 +26,7 @@ | |
| ) | ||
| from graphql_api.helpers.ariadne import ariadne_load_local_graphql | ||
| from graphql_api.helpers.connection import ( | ||
| Connection, | ||
| build_connection_graphql, | ||
| queryset_to_connection, | ||
| ) | ||
|
|
@@ -38,7 +39,7 @@ | |
| from services.profiling import ProfilingSummary | ||
| from services.redis_configuration import get_redis_connection | ||
| from timeseries.helpers import fill_sparse_measurements | ||
| from timeseries.models import Interval, MeasurementSummary | ||
| from timeseries.models import Interval | ||
| from utils.config import get_config | ||
|
|
||
| owner = ariadne_load_local_graphql(__file__, "owner.graphql") | ||
|
|
@@ -51,12 +52,12 @@ | |
| @convert_kwargs_to_snake_case | ||
| def resolve_repositories( | ||
| owner: Owner, | ||
| info, | ||
| filters=None, | ||
| ordering=RepositoryOrdering.ID, | ||
| ordering_direction=OrderingDirection.ASC, | ||
| **kwargs, | ||
| ): | ||
| info: GraphQLResolveInfo, | ||
| filters: Optional[dict] = None, | ||
| ordering: Optional[RepositoryOrdering] = RepositoryOrdering.ID, | ||
| ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC, | ||
| **kwargs: Any, | ||
| ) -> Connection: | ||
| current_owner = info.context["request"].current_owner | ||
| okta_account_auths: list[int] = info.context["request"].session.get( | ||
| OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, [] | ||
|
|
@@ -81,19 +82,19 @@ def resolve_repositories( | |
|
|
||
| @owner_bindable.field("isCurrentUserPartOfOrg") | ||
| @sync_to_async | ||
| def resolve_is_current_user_part_of_org(owner, info: GraphQLResolveInfo): | ||
| def resolve_is_current_user_part_of_org(owner: Owner, info: GraphQLResolveInfo) -> bool: | ||
| current_owner = info.context["request"].current_owner | ||
| return current_user_part_of_org(current_owner, owner) | ||
|
|
||
|
|
||
| @owner_bindable.field("yaml") | ||
| def resolve_yaml(owner: Owner, info: GraphQLResolveInfo): | ||
| def resolve_yaml(owner: Owner, info: GraphQLResolveInfo) -> Optional[str]: | ||
| if owner.yaml is None: | ||
| return | ||
| return None | ||
| current_owner = info.context["request"].current_owner | ||
| if not current_user_part_of_org(current_owner, owner): | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other change. |
||
| return | ||
| return yaml.dump(owner.yaml) | ||
| return None | ||
| return owner.yaml.get("to_string", yaml.dump(owner.yaml)) | ||
|
|
||
|
|
||
| @owner_bindable.field("plan") | ||
|
|
@@ -134,7 +135,9 @@ def resolve_ownerid(owner: Owner, info: GraphQLResolveInfo) -> int: | |
|
|
||
|
|
||
| @owner_bindable.field("repository") | ||
| async def resolve_repository(owner: Owner, info, name): | ||
| async def resolve_repository( | ||
| owner: Owner, info: GraphQLResolveInfo, name: str | ||
| ) -> Repository | NotFoundError: | ||
| command = info.context["executor"].get_command("repository") | ||
| okta_authenticated_accounts: list[int] = info.context["request"].session.get( | ||
| OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, [] | ||
|
|
@@ -174,37 +177,43 @@ async def resolve_repository(owner: Owner, info, name): | |
|
|
||
| @owner_bindable.field("numberOfUploads") | ||
| @require_part_of_org | ||
| async def resolve_number_of_uploads(owner: Owner, info, **kwargs): | ||
| async def resolve_number_of_uploads( | ||
| owner: Owner, info: GraphQLResolveInfo, **kwargs: Any | ||
| ) -> int: | ||
| command = info.context["executor"].get_command("owner") | ||
| return await command.get_uploads_number_per_user(owner) | ||
|
|
||
|
|
||
| @owner_bindable.field("isAdmin") | ||
| @require_part_of_org | ||
| def resolve_is_current_user_an_admin(owner: Owner, info: GraphQLResolveInfo): | ||
| def resolve_is_current_user_an_admin(owner: Owner, info: GraphQLResolveInfo) -> bool: | ||
| current_owner = info.context["request"].current_owner | ||
| command = info.context["executor"].get_command("owner") | ||
| return command.get_is_current_user_an_admin(owner, current_owner) | ||
|
|
||
|
|
||
| @owner_bindable.field("hashOwnerid") | ||
| @require_part_of_org | ||
| def resolve_hash_ownerid(owner: Owner, info: GraphQLResolveInfo): | ||
| def resolve_hash_ownerid(owner: Owner, info: GraphQLResolveInfo) -> str: | ||
| hash_ownerid = sha1(str(owner.ownerid).encode()) | ||
| return hash_ownerid.hexdigest() | ||
|
|
||
|
|
||
| @owner_bindable.field("orgUploadToken") | ||
| @require_part_of_org | ||
| def resolve_org_upload_token(owner: Owner, info, **kwargs): | ||
| def resolve_org_upload_token( | ||
| owner: Owner, info: GraphQLResolveInfo, **kwargs: Any | ||
| ) -> str: | ||
| command = info.context["executor"].get_command("owner") | ||
| return command.get_org_upload_token(owner) | ||
|
|
||
|
|
||
| @owner_bindable.field("defaultOrgUsername") | ||
| @sync_to_async | ||
| @require_part_of_org | ||
| def resolve_org_default_org_username(owner: Owner, info, **kwargs) -> int: | ||
| def resolve_org_default_org_username( | ||
| owner: Owner, info: GraphQLResolveInfo, **kwargs: Any | ||
| ) -> Optional[str]: | ||
| return None if owner.default_org is None else owner.default_org.username | ||
|
|
||
|
|
||
|
|
@@ -213,13 +222,13 @@ def resolve_org_default_org_username(owner: Owner, info, **kwargs) -> int: | |
| @convert_kwargs_to_snake_case | ||
| def resolve_measurements( | ||
| owner: Owner, | ||
| info, | ||
| info: GraphQLResolveInfo, | ||
| interval: Interval, | ||
| after: Optional[datetime] = None, | ||
| before: Optional[datetime] = None, | ||
| repos: Optional[List[str]] = None, | ||
| is_public: Optional[bool] = None, | ||
| ) -> Iterable[MeasurementSummary]: | ||
| ) -> Iterable[dict]: | ||
| current_owner = info.context["request"].current_owner | ||
|
|
||
| okta_authenticated_accounts: list[int] = info.context["request"].session.get( | ||
|
|
@@ -256,7 +265,7 @@ def resolve_measurements( | |
|
|
||
| @owner_bindable.field("isCurrentUserActivated") | ||
| @sync_to_async | ||
| def resolve_is_current_user_activated(owner: Owner, info: GraphQLResolveInfo): | ||
| def resolve_is_current_user_activated(owner: Owner, info: GraphQLResolveInfo) -> bool: | ||
| current_user = info.context["request"].user | ||
| if not current_user.is_authenticated: | ||
| return False | ||
|
|
@@ -303,7 +312,7 @@ def resolve_is_github_rate_limited( | |
| @convert_kwargs_to_snake_case | ||
| def resolve_owner_invoice( | ||
| owner: Owner, | ||
| info, | ||
| info: GraphQLResolveInfo, | ||
| invoice_id: str, | ||
| ) -> stripe.Invoice | None: | ||
| return BillingService(requesting_user=owner).get_invoice(owner, invoice_id) | ||
|
|
@@ -371,7 +380,9 @@ def resolve_ai_enabled_repos( | |
|
|
||
| @owner_bindable.field("uploadTokenRequired") | ||
| @require_part_of_org | ||
| def resolve_upload_token_required(owner: Owner, info) -> bool | None: | ||
| def resolve_upload_token_required( | ||
| owner: Owner, info: GraphQLResolveInfo | ||
| ) -> bool | None: | ||
| return owner.upload_token_required_for_public_repos | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,3 +56,15 @@ def test_when_commit_has_yaml_with_owner(self, mock_fetch_yaml): | |
| """ | ||
| config = yaml.final_commit_yaml(self.commit, self.org) | ||
| assert config["codecov"]["require_ci_to_pass"] is False | ||
|
|
||
| @patch("services.yaml.fetch_current_yaml_from_provider_via_reference") | ||
| def test_when_commit_has_reserved_to_string_key(self, mock_fetch_yaml): | ||
| mock_fetch_yaml.return_value = """ | ||
| codecov: | ||
| notify: | ||
| require_ci_to_pass: no | ||
| to_string: hello | ||
| """ | ||
| config = yaml.final_commit_yaml(self.commit, self.org) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case you didn't see this, I wanted to see what I think it's not a problem because your change only deals with the "ownerYaml". I also checked what other clients than the graphql resolver for the owner yaml in this PR may be accessing the revised owner.yaml field. Searching "owner.yaml" shows nothing in shared and a couple spots in |
||
| assert config.get("to_string") is None | ||
| assert "to_string" not in config.to_dict() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd guess in a perfect world we would want
validate_yaml(in shared) to reject if any reserved keys (liketo_string) are in it. It's probably a lot of work for something unlikely, though so can let it beThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh never mind I had missed the note in the PR description! Sounds like that will get covered later - nice!