Skip to content

Commit a98a6ef

Browse files
committed
Merge remote-tracking branch 'origin/main' into dependabot/pip/requests-gte-2.33.1
2 parents 56ed559 + b7b4810 commit a98a6ef

5 files changed

Lines changed: 44 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **Mypy: graduated `opencontractserver/users/tasks.py` out of the baseline** (Issue #1333 follow-up): `tasks.py` was the last `opencontractserver.users` module still suppressed in `mypy.ini`. PR #1370 left it untyped because the file is only loaded when `settings.USE_AUTH0=True`, so it never failed at runtime under the test settings; the typing gap kept the package short of the issue's "all four packages at ≥80% return-annotation coverage" Done-When criterion. Added return + parameter annotations to all five Auth0 sync tasks (`get_new_auth0_token`, `apply_data_to_user`, `sync_remote_user`, `ensure_valid_auth0_token`, `get_user_details_async`), introduced a module-level docstring documenting the `USE_AUTH0` gating, and removed the `[mypy-opencontractserver.users.tasks] ignore_errors = True` section. Local `data` rebound from request body (`dict[str, str]`) to response payload (`dict[str, Any]`) was split into two distinctly-named variables (`request_data` / `payload`) so the types are unambiguous; behavior is unchanged. No callers needed updating — `config/graphql_auth0_auth/utils.py` still consumes `sync_remote_user.delay(...)` exactly as before.
13+
1014
### Fixed
1115

16+
- **`test_superuser_sees_all_queryset` miscounts personal corpuses by 1** (Issue #1394, `opencontractserver/tests/test_visibility_managers.py`, `opencontractserver/tests/test_resolvers.py`): Two `VisibleToUserTests.test_superuser_sees_all_queryset` cases asserted that `Corpus.objects.visible_to_user(superuser).count() == 4` (public + private + 2 personal), but the actual count is 5 because the test DB starts with a pre-existing personal corpus owned by django-guardian's `AnonymousUser` (created during fixture setup before/around the username-based skip in `opencontractserver/users/signals.py::user_created_signal`). The assertion is now scoped to corpuses created by the test's two users (`creator__in=[self.user, self.superuser]`), making it resilient to any fixture-level corpuses that exist at test DB init time. Production code is unchanged.
1217
- **Merged `frontend` Codecov flag drops to ~33% on every commit where Frontend CI's CT job fails** (`frontend/package.json` `test:coverage:ct`): the script chained `playwright test ... && mkdir -p ... && nyc report ...`, so a failing CT run short-circuited before `nyc report` could turn the per-test JSON files in `.nyc_output` into an `lcov.info`. The downstream `Upload CT Coverage to Codecov` step (`if: success() || failure()`) then errored with "No coverage reports found" and `frontend-component` did not upload for that SHA. Codecov's server-side aggregation of the `frontend` flag was left with only `frontend-unit` (~23%) and `frontend-e2e` (~24%), pulling the merged number down to ~33% even though the previous commit was at ~67% — observed on six consecutive main commits 2026-04-26T01:02..02:58Z (`2d7033f8`..`be5bcfc8`) before recovering on `30298391`. Mirrored the existing `test:e2e:coverage` pattern (`; CT_EXIT=$?; nyc report ... || echo "No coverage data to report"; exit $CT_EXIT`) so `nyc report` runs regardless of test outcome and the lcov ships even on red CT runs. `frontend-component` will still report a slightly lower number when tests fail (failed tests register fewer hits), but it will report — keeping the merged `frontend` flag's denominator stable.
1318
- **`User.__init__` shared-state mutation re-introduced by branch merge** (`opencontractserver/users/models.py:172-180` removed): PR #1374 (commit `50ed6740`) deleted the `User.__init__` override that mutated `Field.validators[0]` on every instantiation, but a subsequent merge (`b68c1cb4 → 6d2cddbf`) resurrected the override along with its mypy-narrowing changes. The current main on commit `6d2cddbf` therefore reproduced the original `#1358` bug: `User(...)` rebound `username_field.validators[0]` and clobbered any third-party validator prepended to the list. Removed the `__init__` override entirely; the class-body declaration `validators=[UserUnicodeUsernameValidator()]` on the `username` field (still present from PR #1374) is the canonical and only declaration. Also dropped the now-unused `Field` import. Regression coverage from PR #1374 (`opencontractserver/tests/test_user_username_validator.py`) was already on main and is what surfaced the regression in CI.
1419

mypy.ini

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,10 +1095,7 @@ ignore_errors = True
10951095
[mypy-opencontractserver.tests.websocket.test_unified_agent_consumer]
10961096
ignore_errors = True
10971097

1098-
# --- opencontractserver.users (1 file) ---
1099-
1100-
[mypy-opencontractserver.users.tasks]
1101-
ignore_errors = True
1098+
# --- opencontractserver.users ---
11021099

11031100
# ``opencontractserver.users.models.User`` defines reverse relations to
11041101
# ``opencontractserver.corpuses.models.Corpus``, ``.CorpusFolder``, and

opencontractserver/tests/test_resolvers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ def test_superuser_sees_all_queryset(self):
5959
"""Superusers should see all objects ordered by creation."""
6060
result = Corpus.objects.visible_to_user(self.superuser)
6161

62+
# Filter to corpuses created by this test's users to make the assertion
63+
# resilient to fixture-level personal corpuses (e.g. the one auto-created
64+
# for guardian's AnonymousUser during DB setup). See issue #1394.
65+
scoped = result.filter(creator__in=[self.user, self.superuser])
66+
6267
# Should see both test corpora + 2 personal corpuses (one per user)
6368
# Each user (user, superuser) gets a personal corpus auto-created
64-
self.assertEqual(result.count(), 4) # public + private + 2 personal
69+
self.assertEqual(scoped.count(), 4) # public + private + 2 personal
6570
# Should be ordered by created
6671
self.assertEqual(result.query.order_by, ("created",))
6772

opencontractserver/tests/test_visibility_managers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,14 @@ def test_superuser_sees_all_queryset(self):
7171
"""Superusers should see all objects ordered by creation."""
7272
result = Corpus.objects.visible_to_user(self.superuser)
7373

74+
# Filter to corpuses created by this test's users to make the assertion
75+
# resilient to fixture-level personal corpuses (e.g. the one auto-created
76+
# for guardian's AnonymousUser during DB setup). See issue #1394.
77+
scoped = result.filter(creator__in=[self.user, self.superuser])
78+
7479
# Should see both test corpora + 2 personal corpuses (one per user)
7580
# Each user (user, superuser) gets a personal corpus auto-created
76-
self.assertEqual(result.count(), 4) # public + private + 2 personal
81+
self.assertEqual(scoped.count(), 4) # public + private + 2 personal
7782
# Should be ordered by created
7883
self.assertEqual(result.query.order_by, ("created",))
7984

opencontractserver/users/tasks.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
"""Celery tasks for synchronising user data with Auth0.
2+
3+
These tasks are defined only when ``settings.USE_AUTH0`` is enabled — they
4+
fetch a Machine-to-Machine token, look up the remote profile via the Auth0
5+
Management API, and copy the result onto the local :class:`User` row.
6+
"""
7+
18
import datetime
29
import json
310
import logging
11+
from typing import Any, Optional
412

513
import requests
614
from celery import chain
15+
from celery.result import AsyncResult
716
from django.conf import settings
817
from django.contrib.auth import get_user_model
918

@@ -21,34 +30,34 @@
2130
if settings.USE_AUTH0:
2231

2332
@celery_app.task()
24-
def get_new_auth0_token():
33+
def get_new_auth0_token() -> Optional[str]:
2534

2635
# print("get_new_auth0_token")
2736
url = f"https://{auth0_settings.AUTH0_DOMAIN}/oauth/token"
2837
# print(url)
2938

30-
headers = {"content-type": "application/json"}
39+
headers: dict[str, str] = {"content-type": "application/json"}
3140
# print(headers)
3241

33-
data = {
42+
request_data: dict[str, str] = {
3443
"grant_type": auth0_settings.AUTH0_M2M_MANAGEMENT_GRANT_TYPE,
3544
"client_id": auth0_settings.AUTH0_M2M_MANAGEMENT_API_ID,
3645
"client_secret": auth0_settings.AUTH0_M2M_MANAGEMENT_API_SECRET,
3746
"audience": f"https://{auth0_settings.AUTH0_DOMAIN}/api/v2/",
3847
}
39-
# print(f"Machine-2-Machine Request data: {data}")
48+
# print(f"Machine-2-Machine Request data: {request_data}")
4049

41-
response = requests.post(url, headers=headers, json=data)
50+
response = requests.post(url, headers=headers, json=request_data)
4251

4352
# print("Auth0 Response:")
4453
# print(response.status_code)
4554
# print(response.text)
4655

4756
if response.status_code == 200:
48-
data = json.loads(response.text)
49-
# print(data)
50-
access_token = data["access_token"]
51-
expires_in = data["expires_in"]
57+
payload: dict[str, Any] = json.loads(response.text)
58+
# print(payload)
59+
access_token: str = payload["access_token"]
60+
expires_in: int = payload["expires_in"]
5261

5362
newToken = Auth0APIToken()
5463
newToken.token = access_token
@@ -60,11 +69,11 @@ def get_new_auth0_token():
6069

6170
return newToken.token
6271

63-
else:
64-
logger.error("Error retrieving access token to Auth0.")
72+
logger.error("Error retrieving access token to Auth0.")
73+
return None
6574

6675
@celery_app.task()
67-
def apply_data_to_user(data, userPk):
76+
def apply_data_to_user(data: dict[str, Any], userPk: str) -> None:
6877

6978
# print(f"apply_data_to_user() - userPk is: {userPk}\nData: {data}")
7079

@@ -103,7 +112,7 @@ def apply_data_to_user(data, userPk):
103112
)
104113

105114
@celery_app.task()
106-
def sync_remote_user(user_pk):
115+
def sync_remote_user(user_pk: str) -> AsyncResult:
107116

108117
refresh = False
109118
tokens = Auth0APIToken.objects.all()
@@ -133,7 +142,7 @@ def sync_remote_user(user_pk):
133142
return data.apply_async()
134143

135144
@celery_app.task()
136-
def ensure_valid_auth0_token():
145+
def ensure_valid_auth0_token() -> Optional[str]:
137146

138147
tokens = Auth0APIToken.objects.all()
139148

@@ -147,6 +156,7 @@ def ensure_valid_auth0_token():
147156
for tok in tokens:
148157
tok.delete()
149158
return get_new_auth0_token.delay().get()
159+
return None
150160
else:
151161
if tokens[0].expiration_Date < datetime.datetime.now(datetime.timezone.utc):
152162
# print("Token has expired. Refetching from Auth0")
@@ -157,8 +167,8 @@ def ensure_valid_auth0_token():
157167
return tokens[0].token
158168

159169
@celery_app.task
160-
def get_user_details_async(token, auth0_Id):
161-
headers = {
170+
def get_user_details_async(token: str, auth0_Id: str) -> dict[str, Any]:
171+
headers: dict[str, str] = {
162172
"Authorization": f"Bearer {token}",
163173
}
164174
url = f"https://{auth0_settings.AUTH0_DOMAIN}/api/v2/users/{auth0_Id}"

0 commit comments

Comments
 (0)