Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 8cf9085

Browse files
committed
Merge branch 'oct_15_comment' into staging
2 parents a89fef1 + 909233d commit 8cf9085

File tree

8 files changed

+204
-37
lines changed

8 files changed

+204
-37
lines changed

codecov_auth/commands/owner/interactors/set_yaml_on_owner.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import html
2+
from typing import Optional
23

34
import yaml
45
from shared.validation.exceptions import InvalidYamlException
@@ -12,26 +13,27 @@
1213
ValidationError,
1314
)
1415
from codecov.db import sync_to_async
16+
from codecov_auth.constants import OWNER_YAML_TO_STRING_KEY
1517
from codecov_auth.helpers import current_user_part_of_org
1618
from codecov_auth.models import Owner
1719

1820

1921
class SetYamlOnOwnerInteractor(BaseInteractor):
20-
def validate(self):
22+
def validate(self) -> None:
2123
if not self.current_user.is_authenticated:
2224
raise Unauthenticated()
2325

24-
def authorize(self):
26+
def authorize(self) -> None:
2527
if not current_user_part_of_org(self.current_owner, self.owner):
2628
raise Unauthorized()
2729

28-
def get_owner(self, username):
30+
def get_owner(self, username: str) -> Owner:
2931
try:
3032
return Owner.objects.get(username=username, service=self.service)
3133
except Owner.DoesNotExist:
3234
raise NotFound()
3335

34-
def convert_yaml_to_dict(self, yaml_input):
36+
def convert_yaml_to_dict(self, yaml_input: str) -> Optional[dict]:
3537
yaml_safe = html.escape(yaml_input, quote=False)
3638
try:
3739
yaml_dict = yaml.safe_load(yaml_safe)
@@ -49,10 +51,12 @@ def convert_yaml_to_dict(self, yaml_input):
4951
raise ValidationError(message)
5052

5153
@sync_to_async
52-
def execute(self, username, yaml_input):
54+
def execute(self, username: str, yaml_input: str) -> Owner:
5355
self.validate()
5456
self.owner = self.get_owner(username)
5557
self.authorize()
5658
self.owner.yaml = self.convert_yaml_to_dict(yaml_input)
59+
if self.owner.yaml:
60+
self.owner.yaml[OWNER_YAML_TO_STRING_KEY] = yaml_input
5761
self.owner.save()
5862
return self.owner

codecov_auth/commands/owner/interactors/tests/test_set_yaml_on_owner.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@
3535
bot: foo: bar
3636
"""
3737

38+
good_yaml_with_comments = """
39+
# comment 1
40+
codecov: # comment 2
41+
42+
43+
bot: 'codecov'
44+
# comment 3
45+
#comment 4
46+
"""
47+
3848

3949
class SetYamlOnOwnerInteractorTest(TransactionTestCase):
4050
def setUp(self):
@@ -69,15 +79,25 @@ async def test_user_is_part_of_org_and_yaml_is_good(self):
6979
)
7080
# check the interactor returns the right owner
7181
assert owner_updated.ownerid == self.org.ownerid
72-
assert owner_updated.yaml == {"codecov": {"require_ci_to_pass": True}}
82+
assert owner_updated.yaml == {
83+
"codecov": {
84+
"require_ci_to_pass": True,
85+
},
86+
"to_string": "\n" "codecov:\n" " require_ci_to_pass: yes\n",
87+
}
7388

7489
async def test_user_is_part_of_org_and_yaml_has_quotes(self):
7590
owner_updated = await self.execute(
7691
self.current_owner, self.org.username, good_yaml_with_quotes
7792
)
7893
# check the interactor returns the right owner
7994
assert owner_updated.ownerid == self.org.ownerid
80-
assert owner_updated.yaml == {"codecov": {"bot": "codecov"}}
95+
assert owner_updated.yaml == {
96+
"codecov": {
97+
"bot": "codecov",
98+
},
99+
"to_string": "\n" "codecov:\n" " bot: 'codecov'\n",
100+
}
81101

82102
async def test_user_is_part_of_org_and_yaml_is_empty(self):
83103
owner_updated = await self.execute(self.current_owner, self.org.username, "")
@@ -103,3 +123,23 @@ async def test_yaml_syntax_error(self):
103123
str(e.value)
104124
== "Syntax error at line 3, column 13: mapping values are not allowed here"
105125
)
126+
127+
async def test_yaml_has_comments(self):
128+
owner_updated = await self.execute(
129+
self.current_owner, self.org.username, good_yaml_with_comments
130+
)
131+
# check the interactor returns the right owner
132+
assert owner_updated.ownerid == self.org.ownerid
133+
assert owner_updated.yaml == {
134+
"codecov": {
135+
"bot": "codecov",
136+
},
137+
"to_string": "\n"
138+
"# comment 1\n"
139+
"codecov: # comment 2\n"
140+
"\n"
141+
"\n"
142+
" bot: 'codecov'\n"
143+
"# comment 3\n"
144+
" #comment 4\n",
145+
}

codecov_auth/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
GITLAB_BASE_URL = "https://gitlab.com"
44
GRAVATAR_BASE_URL = "https://www.gravatar.com"
55
AVATARIO_BASE_URL = "https://avatars.io"
6+
OWNER_YAML_TO_STRING_KEY = "to_string"

docker/Dockerfile.requirements

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ RUN apt-get install -y \
2525
libpq-dev \
2626
make \
2727
curl \
28+
libexpat1 \
2829
&& pip install --upgrade pip
2930

3031
WORKDIR /pip-packages/

graphql_api/types/owner/owner.py

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from datetime import datetime
22
from hashlib import sha1
3-
from typing import Iterable, List, Optional
3+
from typing import Any, Iterable, List, Optional
44

55
import shared.rate_limits as rate_limits
66
import stripe
@@ -11,6 +11,7 @@
1111
import services.activation as activation
1212
import timeseries.helpers as timeseries_helpers
1313
from codecov.db import sync_to_async
14+
from codecov_auth.constants import OWNER_YAML_TO_STRING_KEY
1415
from codecov_auth.helpers import current_user_part_of_org
1516
from codecov_auth.models import (
1617
SERVICE_GITHUB,
@@ -26,6 +27,7 @@
2627
)
2728
from graphql_api.helpers.ariadne import ariadne_load_local_graphql
2829
from graphql_api.helpers.connection import (
30+
Connection,
2931
build_connection_graphql,
3032
queryset_to_connection,
3133
)
@@ -38,7 +40,7 @@
3840
from services.profiling import ProfilingSummary
3941
from services.redis_configuration import get_redis_connection
4042
from timeseries.helpers import fill_sparse_measurements
41-
from timeseries.models import Interval, MeasurementSummary
43+
from timeseries.models import Interval
4244
from utils.config import get_config
4345

4446
owner = ariadne_load_local_graphql(__file__, "owner.graphql")
@@ -51,12 +53,12 @@
5153
@convert_kwargs_to_snake_case
5254
def resolve_repositories(
5355
owner: Owner,
54-
info,
55-
filters=None,
56-
ordering=RepositoryOrdering.ID,
57-
ordering_direction=OrderingDirection.ASC,
58-
**kwargs,
59-
):
56+
info: GraphQLResolveInfo,
57+
filters: Optional[dict] = None,
58+
ordering: Optional[RepositoryOrdering] = RepositoryOrdering.ID,
59+
ordering_direction: Optional[OrderingDirection] = OrderingDirection.ASC,
60+
**kwargs: Any,
61+
) -> Connection:
6062
current_owner = info.context["request"].current_owner
6163
okta_account_auths: list[int] = info.context["request"].session.get(
6264
OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, []
@@ -81,19 +83,19 @@ def resolve_repositories(
8183

8284
@owner_bindable.field("isCurrentUserPartOfOrg")
8385
@sync_to_async
84-
def resolve_is_current_user_part_of_org(owner, info: GraphQLResolveInfo):
86+
def resolve_is_current_user_part_of_org(owner: Owner, info: GraphQLResolveInfo) -> bool:
8587
current_owner = info.context["request"].current_owner
8688
return current_user_part_of_org(current_owner, owner)
8789

8890

8991
@owner_bindable.field("yaml")
90-
def resolve_yaml(owner: Owner, info: GraphQLResolveInfo):
92+
def resolve_yaml(owner: Owner, info: GraphQLResolveInfo) -> Optional[str]:
9193
if owner.yaml is None:
92-
return
94+
return None
9395
current_owner = info.context["request"].current_owner
9496
if not current_user_part_of_org(current_owner, owner):
95-
return
96-
return yaml.dump(owner.yaml)
97+
return None
98+
return owner.yaml.get(OWNER_YAML_TO_STRING_KEY, yaml.dump(owner.yaml))
9799

98100

99101
@owner_bindable.field("plan")
@@ -134,7 +136,9 @@ def resolve_ownerid(owner: Owner, info: GraphQLResolveInfo) -> int:
134136

135137

136138
@owner_bindable.field("repository")
137-
async def resolve_repository(owner: Owner, info, name):
139+
async def resolve_repository(
140+
owner: Owner, info: GraphQLResolveInfo, name: str
141+
) -> Repository | NotFoundError:
138142
command = info.context["executor"].get_command("repository")
139143
okta_authenticated_accounts: list[int] = info.context["request"].session.get(
140144
OKTA_SIGNED_IN_ACCOUNTS_SESSION_KEY, []
@@ -174,37 +178,43 @@ async def resolve_repository(owner: Owner, info, name):
174178

175179
@owner_bindable.field("numberOfUploads")
176180
@require_part_of_org
177-
async def resolve_number_of_uploads(owner: Owner, info, **kwargs):
181+
async def resolve_number_of_uploads(
182+
owner: Owner, info: GraphQLResolveInfo, **kwargs: Any
183+
) -> int:
178184
command = info.context["executor"].get_command("owner")
179185
return await command.get_uploads_number_per_user(owner)
180186

181187

182188
@owner_bindable.field("isAdmin")
183189
@require_part_of_org
184-
def resolve_is_current_user_an_admin(owner: Owner, info: GraphQLResolveInfo):
190+
def resolve_is_current_user_an_admin(owner: Owner, info: GraphQLResolveInfo) -> bool:
185191
current_owner = info.context["request"].current_owner
186192
command = info.context["executor"].get_command("owner")
187193
return command.get_is_current_user_an_admin(owner, current_owner)
188194

189195

190196
@owner_bindable.field("hashOwnerid")
191197
@require_part_of_org
192-
def resolve_hash_ownerid(owner: Owner, info: GraphQLResolveInfo):
198+
def resolve_hash_ownerid(owner: Owner, info: GraphQLResolveInfo) -> str:
193199
hash_ownerid = sha1(str(owner.ownerid).encode())
194200
return hash_ownerid.hexdigest()
195201

196202

197203
@owner_bindable.field("orgUploadToken")
198204
@require_part_of_org
199-
def resolve_org_upload_token(owner: Owner, info, **kwargs):
205+
def resolve_org_upload_token(
206+
owner: Owner, info: GraphQLResolveInfo, **kwargs: Any
207+
) -> str:
200208
command = info.context["executor"].get_command("owner")
201209
return command.get_org_upload_token(owner)
202210

203211

204212
@owner_bindable.field("defaultOrgUsername")
205213
@sync_to_async
206214
@require_part_of_org
207-
def resolve_org_default_org_username(owner: Owner, info, **kwargs) -> int:
215+
def resolve_org_default_org_username(
216+
owner: Owner, info: GraphQLResolveInfo, **kwargs: Any
217+
) -> Optional[str]:
208218
return None if owner.default_org is None else owner.default_org.username
209219

210220

@@ -213,13 +223,13 @@ def resolve_org_default_org_username(owner: Owner, info, **kwargs) -> int:
213223
@convert_kwargs_to_snake_case
214224
def resolve_measurements(
215225
owner: Owner,
216-
info,
226+
info: GraphQLResolveInfo,
217227
interval: Interval,
218228
after: Optional[datetime] = None,
219229
before: Optional[datetime] = None,
220230
repos: Optional[List[str]] = None,
221231
is_public: Optional[bool] = None,
222-
) -> Iterable[MeasurementSummary]:
232+
) -> Iterable[dict]:
223233
current_owner = info.context["request"].current_owner
224234

225235
okta_authenticated_accounts: list[int] = info.context["request"].session.get(
@@ -256,7 +266,7 @@ def resolve_measurements(
256266

257267
@owner_bindable.field("isCurrentUserActivated")
258268
@sync_to_async
259-
def resolve_is_current_user_activated(owner: Owner, info: GraphQLResolveInfo):
269+
def resolve_is_current_user_activated(owner: Owner, info: GraphQLResolveInfo) -> bool:
260270
current_user = info.context["request"].user
261271
if not current_user.is_authenticated:
262272
return False
@@ -303,7 +313,7 @@ def resolve_is_github_rate_limited(
303313
@convert_kwargs_to_snake_case
304314
def resolve_owner_invoice(
305315
owner: Owner,
306-
info,
316+
info: GraphQLResolveInfo,
307317
invoice_id: str,
308318
) -> stripe.Invoice | None:
309319
return BillingService(requesting_user=owner).get_invoice(owner, invoice_id)
@@ -371,7 +381,9 @@ def resolve_ai_enabled_repos(
371381

372382
@owner_bindable.field("uploadTokenRequired")
373383
@require_part_of_org
374-
def resolve_upload_token_required(owner: Owner, info) -> bool | None:
384+
def resolve_upload_token_required(
385+
owner: Owner, info: GraphQLResolveInfo
386+
) -> bool | None:
375387
return owner.upload_token_required_for_public_repos
376388

377389

services/tests/test_yaml.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,15 @@ def test_when_commit_has_yaml_with_owner(self, mock_fetch_yaml):
5656
"""
5757
config = yaml.final_commit_yaml(self.commit, self.org)
5858
assert config["codecov"]["require_ci_to_pass"] is False
59+
60+
@patch("services.yaml.fetch_current_yaml_from_provider_via_reference")
61+
def test_when_commit_has_reserved_to_string_key(self, mock_fetch_yaml):
62+
mock_fetch_yaml.return_value = """
63+
codecov:
64+
notify:
65+
require_ci_to_pass: no
66+
to_string: hello
67+
"""
68+
config = yaml.final_commit_yaml(self.commit, self.org)
69+
assert config.get("to_string") is None
70+
assert "to_string" not in config.to_dict()

0 commit comments

Comments
 (0)