chore(): switched all fields in contract to snake_case and linting improvements

This commit is contained in:
ITQ
2026-02-12 22:17:55 +03:00
parent 102f425259
commit 362398d56b
32 changed files with 401 additions and 353 deletions
+5 -3
View File
@@ -1,4 +1,5 @@
import json
from typing import override
from django.test import Client, TestCase
from django.urls import reverse
@@ -7,13 +8,14 @@ from apps.users.auth.jwt import (
create_token_pair,
)
from apps.users.models import User, UserRole
from apps.users.tests._helpers import _auth_header, _make_user
from apps.users.tests.helpers import auth_header, make_user
class AuthAPITest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.user: User = _make_user(
self.user: User = make_user(
username="api_auth",
email="api_auth@x.com",
password="testpass123",
@@ -92,7 +94,7 @@ class AuthAPITest(TestCase):
def test_me_authenticated(self) -> None:
resp = self.client.get(
reverse("api-1:me"),
HTTP_AUTHORIZATION=_auth_header(self.user),
HTTP_AUTHORIZATION=auth_header(self.user),
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
+3 -3
View File
@@ -47,7 +47,7 @@ def handle_validation_error(
{
"field": field,
"issue": error.get("msg", "Unknown error"),
"rejectedValue": error.get("input"),
"rejected_value": error.get("input"),
}
)
@@ -77,7 +77,7 @@ def handle_django_validation_error(
{
"field": field,
"issue": str(error.message),
"rejectedValue": None,
"rejected_value": None,
}
for error in errors
)
@@ -86,7 +86,7 @@ def handle_django_validation_error(
{
"field": "non_field_error",
"issue": str(error.message),
"rejectedValue": None,
"rejected_value": None,
}
for error in exc.error_list
)
-11
View File
@@ -9,8 +9,6 @@ from apps.users.models import User
class ApproverOut(ModelSchema):
first_name: str = Field("", alias="firstName")
last_name: str = Field("", alias="lastName")
class Meta:
model = User
@@ -55,12 +53,10 @@ class ApproverGroupOut(ModelSchema):
class ApproverGroupCreateIn(Schema):
experimenter_id: str = Field(
...,
alias="experimenterId",
description="UUID of the experimenter user this group belongs to.",
)
approver_ids: list[str] = Field(
default_factory=list,
alias="approverIds",
description=(
"List of user UUIDs to add as approvers. "
"Each user must have the 'approver' role."
@@ -68,7 +64,6 @@ class ApproverGroupCreateIn(Schema):
)
min_approvals: int = Field(
1,
alias="minApprovals",
ge=1,
description=(
"Number of distinct approvals required. "
@@ -80,7 +75,6 @@ class ApproverGroupCreateIn(Schema):
class ApproverGroupUpdateIn(Schema):
approver_ids: list[str] | None = Field(
None,
alias="approverIds",
description=(
"If provided, replaces the current set of approvers. "
"Each user must have the 'approver' role."
@@ -88,7 +82,6 @@ class ApproverGroupUpdateIn(Schema):
)
min_approvals: int | None = Field(
None,
alias="minApprovals",
ge=1,
description="New minimum approval threshold.",
)
@@ -102,7 +95,6 @@ class ApproverGroupListOut(Schema):
class ApproverGroupAddApproverIn(Schema):
approver_id: str = Field(
...,
alias="approverId",
description="UUID of the user to add as an approver.",
)
@@ -110,7 +102,6 @@ class ApproverGroupAddApproverIn(Schema):
class ApproverGroupRemoveApproverIn(Schema):
approver_id: str = Field(
...,
alias="approverId",
description="UUID of the approver to remove.",
)
@@ -133,13 +124,11 @@ class ReviewSettingsOut(ModelSchema):
class ReviewSettingsUpdateIn(Schema):
default_min_approvals: int | None = Field(
None,
alias="defaultMinApprovals",
ge=1,
description="New default minimum approval threshold.",
)
allow_any_approver: bool | None = Field(
None,
alias="allowAnyApprover",
description="New fallback policy for approver eligibility.",
)
@@ -1,34 +1,35 @@
import json
import uuid
from typing import override
from django.test import Client, TestCase
from django.urls import reverse
from apps.reviews.models import ApproverGroup
from apps.reviews.services import approver_group_create, review_settings_update
from apps.reviews.tests._helpers import (
_get,
_make_admin,
_make_approver,
_make_experimenter,
_make_viewer,
from apps.reviews.tests.helpers import (
make_admin,
make_approver,
make_experimenter,
make_viewer,
)
from apps.users.models import User
from apps.users.tests._helpers import _auth_header
from apps.users.tests.helpers import auth_header
class ApproverGroupAPITest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.admin: User = _make_admin("_api")
self.viewer: User = _make_viewer("_api")
self.experimenter: User = _make_experimenter("_api")
self.approver1: User = _make_approver("_api1")
self.approver2: User = _make_approver("_api2")
self.approver3: User = _make_approver("_api3")
self.admin_auth: str = _auth_header(self.admin)
self.viewer_auth: str = _auth_header(self.viewer)
self.exp_auth: str = _auth_header(self.experimenter)
self.admin: User = make_admin("_api")
self.viewer: User = make_viewer("_api")
self.experimenter: User = make_experimenter("_api")
self.approver1: User = make_approver("_api1")
self.approver2: User = make_approver("_api2")
self.approver3: User = make_approver("_api3")
self.admin_auth: str = auth_header(self.admin)
self.viewer_auth: str = auth_header(self.viewer)
self.exp_auth: str = auth_header(self.experimenter)
def test_list_groups_admin(self) -> None:
approver_group_create(
@@ -57,7 +58,7 @@ class ApproverGroupAPITest(TestCase):
self.assertEqual(resp.status_code, 401)
def test_list_groups_pagination(self) -> None:
exp2: User = _make_experimenter("_api2")
exp2: User = make_experimenter("_api2")
approver_group_create(experimenter=self.experimenter)
approver_group_create(experimenter=exp2)
resp = self.client.get(
@@ -74,12 +75,12 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(self.experimenter.pk),
"approverIds": [
"experimenter_id": str(self.experimenter.pk),
"approver_ids": [
str(self.approver1.pk),
str(self.approver2.pk),
],
"minApprovals": 2,
"min_approvals": 2,
}
),
content_type="application/json",
@@ -87,7 +88,7 @@ class ApproverGroupAPITest(TestCase):
)
self.assertEqual(resp.status_code, 201)
data = resp.json()
self.assertEqual(_get(data, "minApprovals", "min_approvals"), 2)
self.assertEqual(data["min_approvals"], 2)
self.assertEqual(len(data["approvers"]), 2)
self.assertEqual(data["experimenter"]["id"], str(self.experimenter.pk))
@@ -96,8 +97,8 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(self.experimenter.pk),
"minApprovals": 1,
"experimenter_id": str(self.experimenter.pk),
"min_approvals": 1,
}
),
content_type="application/json",
@@ -112,8 +113,8 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(self.experimenter.pk),
"minApprovals": 1,
"experimenter_id": str(self.experimenter.pk),
"min_approvals": 1,
}
),
content_type="application/json",
@@ -126,8 +127,8 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(self.experimenter.pk),
"minApprovals": 1,
"experimenter_id": str(self.experimenter.pk),
"min_approvals": 1,
}
),
content_type="application/json",
@@ -140,8 +141,8 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(uuid.uuid4()),
"minApprovals": 1,
"experimenter_id": str(uuid.uuid4()),
"min_approvals": 1,
}
),
content_type="application/json",
@@ -154,8 +155,8 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(self.viewer.pk),
"minApprovals": 1,
"experimenter_id": str(self.viewer.pk),
"min_approvals": 1,
}
),
content_type="application/json",
@@ -168,8 +169,8 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(self.experimenter.pk),
"minApprovals": 1,
"experimenter_id": str(self.experimenter.pk),
"min_approvals": 1,
}
),
content_type="application/json",
@@ -179,8 +180,8 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(self.experimenter.pk),
"minApprovals": 1,
"experimenter_id": str(self.experimenter.pk),
"min_approvals": 1,
}
),
content_type="application/json",
@@ -193,9 +194,9 @@ class ApproverGroupAPITest(TestCase):
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(self.experimenter.pk),
"approverIds": [str(self.viewer.pk)],
"minApprovals": 1,
"experimenter_id": str(self.experimenter.pk),
"approver_ids": [str(self.viewer.pk)],
"min_approvals": 1,
}
),
content_type="application/json",
@@ -203,7 +204,7 @@ class ApproverGroupAPITest(TestCase):
)
self.assertIn(resp.status_code, [422, 400])
def test_get_group_admin(self) -> None:
def testget_group_admin(self) -> None:
group: ApproverGroup = approver_group_create(
experimenter=self.experimenter,
approver_ids=[str(self.approver1.pk)],
@@ -220,7 +221,7 @@ class ApproverGroupAPITest(TestCase):
self.assertEqual(data["id"], str(group.pk))
self.assertEqual(len(data["approvers"]), 1)
def test_get_group_not_found(self) -> None:
def testget_group_not_found(self) -> None:
resp = self.client.get(
reverse(
"api-1:get_approver_group",
@@ -230,7 +231,7 @@ class ApproverGroupAPITest(TestCase):
)
self.assertEqual(resp.status_code, 404)
def test_get_group_viewer_denied(self) -> None:
def testget_group_viewer_denied(self) -> None:
group: ApproverGroup = approver_group_create(
experimenter=self.experimenter
)
@@ -256,11 +257,11 @@ class ApproverGroupAPITest(TestCase):
),
data=json.dumps(
{
"approverIds": [
"approver_ids": [
str(self.approver1.pk),
str(self.approver2.pk),
],
"minApprovals": 2,
"min_approvals": 2,
}
),
content_type="application/json",
@@ -268,7 +269,7 @@ class ApproverGroupAPITest(TestCase):
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertEqual(_get(data, "minApprovals", "min_approvals"), 2)
self.assertEqual(data["min_approvals"], 2)
self.assertEqual(len(data["approvers"]), 2)
def test_update_group_partial_min_approvals(self) -> None:
@@ -285,13 +286,13 @@ class ApproverGroupAPITest(TestCase):
"api-1:update_approver_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"minApprovals": 2}),
data=json.dumps({"min_approvals": 2}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertEqual(_get(data, "minApprovals", "min_approvals"), 2)
self.assertEqual(data["min_approvals"], 2)
def test_update_group_not_found(self) -> None:
resp = self.client.patch(
@@ -299,7 +300,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:update_approver_group",
kwargs={"group_id": str(uuid.uuid4())},
),
data=json.dumps({"minApprovals": 1}),
data=json.dumps({"min_approvals": 1}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -314,7 +315,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:update_approver_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"minApprovals": 1}),
data=json.dumps({"min_approvals": 1}),
content_type="application/json",
HTTP_AUTHORIZATION=self.viewer_auth,
)
@@ -368,7 +369,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:add_approver_to_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(self.approver2.pk)}),
data=json.dumps({"approver_id": str(self.approver2.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -381,7 +382,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:add_approver_to_group",
kwargs={"group_id": str(uuid.uuid4())},
),
data=json.dumps({"approverId": str(self.approver1.pk)}),
data=json.dumps({"approver_id": str(self.approver1.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -396,7 +397,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:add_approver_to_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(uuid.uuid4())}),
data=json.dumps({"approver_id": str(uuid.uuid4())}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -411,7 +412,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:add_approver_to_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(self.viewer.pk)}),
data=json.dumps({"approver_id": str(self.viewer.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -427,7 +428,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:add_approver_to_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(self.approver1.pk)}),
data=json.dumps({"approver_id": str(self.approver1.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -442,7 +443,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:add_approver_to_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(self.approver1.pk)}),
data=json.dumps({"approver_id": str(self.approver1.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.viewer_auth,
)
@@ -462,7 +463,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:remove_approver_from_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(self.approver2.pk)}),
data=json.dumps({"approver_id": str(self.approver2.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -480,7 +481,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:remove_approver_from_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(self.approver1.pk)}),
data=json.dumps({"approver_id": str(self.approver1.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -496,7 +497,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:remove_approver_from_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(self.approver2.pk)}),
data=json.dumps({"approver_id": str(self.approver2.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -515,7 +516,7 @@ class ApproverGroupAPITest(TestCase):
"api-1:remove_approver_from_group",
kwargs={"group_id": str(group.pk)},
),
data=json.dumps({"approverId": str(self.approver1.pk)}),
data=json.dumps({"approver_id": str(self.approver1.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=self.viewer_auth,
)
@@ -523,14 +524,15 @@ class ApproverGroupAPITest(TestCase):
class ApproverGroupByExperimenterAPITest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.admin: User = _make_admin("_byexp")
self.experimenter: User = _make_experimenter("_byexp")
self.approver: User = _make_approver("_byexp")
self.admin_auth: str = _auth_header(self.admin)
self.admin: User = make_admin("_byexp")
self.experimenter: User = make_experimenter("_byexp")
self.approver: User = make_approver("_byexp")
self.admin_auth: str = auth_header(self.admin)
def test_get_by_experimenter_found(self) -> None:
def testget_by_experimenter_found(self) -> None:
group: ApproverGroup = approver_group_create(
experimenter=self.experimenter,
approver_ids=[str(self.approver.pk)],
@@ -545,8 +547,8 @@ class ApproverGroupByExperimenterAPITest(TestCase):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.json()["id"], str(group.pk))
def test_get_by_experimenter_not_found(self) -> None:
exp2: User = _make_experimenter("_byexp2")
def testget_by_experimenter_not_found(self) -> None:
exp2: User = make_experimenter("_byexp2")
resp = self.client.get(
reverse(
"api-1:get_approver_group_by_experimenter",
@@ -556,7 +558,7 @@ class ApproverGroupByExperimenterAPITest(TestCase):
)
self.assertEqual(resp.status_code, 404)
def test_get_by_experimenter_invalid_id(self) -> None:
def testget_by_experimenter_invalid_id(self) -> None:
resp = self.client.get(
reverse(
"api-1:get_approver_group_by_experimenter",
@@ -566,7 +568,7 @@ class ApproverGroupByExperimenterAPITest(TestCase):
)
self.assertEqual(resp.status_code, 404)
def test_get_by_experimenter_unauthenticated(self) -> None:
def testget_by_experimenter_unauthenticated(self) -> None:
resp = self.client.get(
reverse(
"api-1:get_approver_group_by_experimenter",
@@ -577,30 +579,29 @@ class ApproverGroupByExperimenterAPITest(TestCase):
class ReviewSettingsAPITest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.admin: User = _make_admin("_rs")
self.viewer: User = _make_viewer("_rs")
self.experimenter: User = _make_experimenter("_rs")
self.approver: User = _make_approver("_rs")
self.admin_auth: str = _auth_header(self.admin)
self.viewer_auth: str = _auth_header(self.viewer)
self.exp_auth: str = _auth_header(self.experimenter)
self.appr_auth: str = _auth_header(self.approver)
self.admin: User = make_admin("_rs")
self.viewer: User = make_viewer("_rs")
self.experimenter: User = make_experimenter("_rs")
self.approver: User = make_approver("_rs")
self.admin_auth: str = auth_header(self.admin)
self.viewer_auth: str = auth_header(self.viewer)
self.exp_auth: str = auth_header(self.experimenter)
self.appr_auth: str = auth_header(self.approver)
def test_get_settings_admin(self) -> None:
def testget_settings_admin(self) -> None:
resp = self.client.get(
reverse("api-1:get_review_settings"),
HTTP_AUTHORIZATION=self.admin_auth,
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
min_val = _get(data, "defaultMinApprovals", "default_min_approvals")
any_val = _get(data, "allowAnyApprover", "allow_any_approver")
self.assertEqual(min_val, 1)
self.assertFalse(any_val)
self.assertEqual(data["default_min_approvals"], 1)
self.assertFalse(data["allow_any_approver"])
def test_get_settings_any_authenticated_role(self) -> None:
def testget_settings_any_authenticated_role(self) -> None:
for auth in [self.viewer_auth, self.exp_auth, self.appr_auth]:
resp = self.client.get(
reverse("api-1:get_review_settings"),
@@ -608,7 +609,7 @@ class ReviewSettingsAPITest(TestCase):
)
self.assertEqual(resp.status_code, 200)
def test_get_settings_unauthenticated(self) -> None:
def testget_settings_unauthenticated(self) -> None:
resp = self.client.get(reverse("api-1:get_review_settings"))
self.assertEqual(resp.status_code, 401)
@@ -616,34 +617,31 @@ class ReviewSettingsAPITest(TestCase):
resp = self.client.put(
reverse("api-1:update_review_settings"),
data=json.dumps(
{"defaultMinApprovals": 3, "allowAnyApprover": False}
{"default_min_approvals": 3, "allow_any_approver": False}
),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
min_val = _get(data, "defaultMinApprovals", "default_min_approvals")
any_val = _get(data, "allowAnyApprover", "allow_any_approver")
self.assertEqual(min_val, 3)
self.assertFalse(any_val)
self.assertEqual(data["default_min_approvals"], 3)
self.assertFalse(data["allow_any_approver"])
def test_update_settings_partial(self) -> None:
resp = self.client.put(
reverse("api-1:update_review_settings"),
data=json.dumps({"defaultMinApprovals": 5}),
data=json.dumps({"default_min_approvals": 5}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
min_val = _get(data, "defaultMinApprovals", "default_min_approvals")
self.assertEqual(min_val, 5)
self.assertEqual(data["default_min_approvals"], 5)
def test_update_settings_viewer_denied(self) -> None:
resp = self.client.put(
reverse("api-1:update_review_settings"),
data=json.dumps({"defaultMinApprovals": 2}),
data=json.dumps({"default_min_approvals": 2}),
content_type="application/json",
HTTP_AUTHORIZATION=self.viewer_auth,
)
@@ -652,7 +650,7 @@ class ReviewSettingsAPITest(TestCase):
def test_update_settings_experimenter_denied(self) -> None:
resp = self.client.put(
reverse("api-1:update_review_settings"),
data=json.dumps({"defaultMinApprovals": 2}),
data=json.dumps({"default_min_approvals": 2}),
content_type="application/json",
HTTP_AUTHORIZATION=self.exp_auth,
)
@@ -661,7 +659,7 @@ class ReviewSettingsAPITest(TestCase):
def test_update_settings_approver_denied(self) -> None:
resp = self.client.put(
reverse("api-1:update_review_settings"),
data=json.dumps({"defaultMinApprovals": 2}),
data=json.dumps({"default_min_approvals": 2}),
content_type="application/json",
HTTP_AUTHORIZATION=self.appr_auth,
)
@@ -670,7 +668,7 @@ class ReviewSettingsAPITest(TestCase):
def test_update_settings_invalid_min_approvals(self) -> None:
resp = self.client.put(
reverse("api-1:update_review_settings"),
data=json.dumps({"defaultMinApprovals": 0}),
data=json.dumps({"default_min_approvals": 0}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -678,15 +676,16 @@ class ReviewSettingsAPITest(TestCase):
class EffectivePolicyAPITest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.admin: User = _make_admin("_pol")
self.experimenter: User = _make_experimenter("_pol")
self.exp_no_group: User = _make_experimenter("_pol2")
self.approver: User = _make_approver("_pol")
self.admin_auth: str = _auth_header(self.admin)
self.viewer: User = _make_viewer("_pol")
self.viewer_auth: str = _auth_header(self.viewer)
self.admin: User = make_admin("_pol")
self.experimenter: User = make_experimenter("_pol")
self.exp_no_group: User = make_experimenter("_pol2")
self.approver: User = make_approver("_pol")
self.admin_auth: str = auth_header(self.admin)
self.viewer: User = make_viewer("_pol")
self.viewer_auth: str = auth_header(self.viewer)
self.group: ApproverGroup = approver_group_create(
experimenter=self.experimenter,
@@ -704,13 +703,10 @@ class EffectivePolicyAPITest(TestCase):
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
exp_id = _get(data, "experimenterId", "experimenter_id")
min_val = _get(data, "minApprovals", "min_approvals")
has_group = _get(data, "hasExplicitGroup", "has_explicit_group")
self.assertEqual(exp_id, str(self.experimenter.pk))
self.assertEqual(min_val, 1)
self.assertEqual(data["experimenter_id"], str(self.experimenter.pk))
self.assertEqual(data["min_approvals"], 1)
self.assertEqual(data["source"], "approver_group")
self.assertTrue(has_group)
self.assertTrue(data["has_explicit_group"])
self.assertEqual(len(data["approvers"]), 1)
self.assertEqual(data["approvers"][0]["id"], str(self.approver.pk))
@@ -727,11 +723,9 @@ class EffectivePolicyAPITest(TestCase):
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
min_val = _get(data, "minApprovals", "min_approvals")
has_group = _get(data, "hasExplicitGroup", "has_explicit_group")
self.assertEqual(data["source"], "global_fallback")
self.assertFalse(has_group)
self.assertEqual(min_val, 2)
self.assertFalse(data["has_explicit_group"])
self.assertEqual(data["min_approvals"], 2)
def test_effective_policy_fallback_deny(self) -> None:
review_settings_update(allow_any_approver=False)
@@ -1,4 +1,5 @@
import json
from typing import override
from django.core.exceptions import ValidationError
from django.test import Client, TestCase
@@ -15,27 +16,28 @@ from apps.reviews.services import (
approver_group_update,
review_settings_update,
)
from apps.reviews.tests._helpers import (
_make_admin,
_make_approver,
_make_experimenter,
_make_viewer,
from apps.reviews.tests.helpers import (
make_admin,
make_approver,
make_experimenter,
make_viewer,
)
from apps.users.models import User
from apps.users.tests._helpers import _auth_header
from apps.users.tests.helpers import auth_header
class ReviewRBACTest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.admin: User = _make_admin("_rbac")
self.experimenter: User = _make_experimenter("_rbac")
self.approver: User = _make_approver("_rbac")
self.viewer: User = _make_viewer("_rbac")
self.admin: User = make_admin("_rbac")
self.experimenter: User = make_experimenter("_rbac")
self.approver: User = make_approver("_rbac")
self.viewer: User = make_viewer("_rbac")
self.non_admin_users: dict[str, str] = {
"experimenter": _auth_header(self.experimenter),
"approver": _auth_header(self.approver),
"viewer": _auth_header(self.viewer),
"experimenter": auth_header(self.experimenter),
"approver": auth_header(self.approver),
"viewer": auth_header(self.viewer),
}
self.group: ApproverGroup = approver_group_create(
experimenter=self.experimenter,
@@ -52,13 +54,13 @@ class ReviewRBACTest(TestCase):
def test_create_group_denied_for_non_admins(self) -> None:
for role_name, auth in self.non_admin_users.items():
exp2: User = _make_experimenter(f"_rbac_cr_{role_name}")
exp2: User = make_experimenter(f"_rbac_cr_{role_name}")
resp = self.client.post(
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(exp2.pk),
"minApprovals": 1,
"experimenter_id": str(exp2.pk),
"min_approvals": 1,
}
),
content_type="application/json",
@@ -84,7 +86,7 @@ class ReviewRBACTest(TestCase):
"api-1:update_approver_group",
kwargs={"group_id": str(self.group.pk)},
),
data=json.dumps({"minApprovals": 1}),
data=json.dumps({"min_approvals": 1}),
content_type="application/json",
HTTP_AUTHORIZATION=auth,
)
@@ -102,14 +104,14 @@ class ReviewRBACTest(TestCase):
self.assertEqual(resp.status_code, 403, role_name)
def test_add_approver_denied_for_non_admins(self) -> None:
appr2: User = _make_approver("_rbac_add")
appr2: User = make_approver("_rbac_add")
for role_name, auth in self.non_admin_users.items():
resp = self.client.post(
reverse(
"api-1:add_approver_to_group",
kwargs={"group_id": str(self.group.pk)},
),
data=json.dumps({"approverId": str(appr2.pk)}),
data=json.dumps({"approver_id": str(appr2.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=auth,
)
@@ -122,7 +124,7 @@ class ReviewRBACTest(TestCase):
"api-1:remove_approver_from_group",
kwargs={"group_id": str(self.group.pk)},
),
data=json.dumps({"approverId": str(self.approver.pk)}),
data=json.dumps({"approver_id": str(self.approver.pk)}),
content_type="application/json",
HTTP_AUTHORIZATION=auth,
)
@@ -132,7 +134,7 @@ class ReviewRBACTest(TestCase):
for role_name, auth in self.non_admin_users.items():
resp = self.client.put(
reverse("api-1:update_review_settings"),
data=json.dumps({"defaultMinApprovals": 99}),
data=json.dumps({"default_min_approvals": 99}),
content_type="application/json",
HTTP_AUTHORIZATION=auth,
)
@@ -140,14 +142,15 @@ class ReviewRBACTest(TestCase):
class ReviewEdgeCasesTest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.admin: User = _make_admin("_edge")
self.admin_auth: str = _auth_header(self.admin)
self.admin: User = make_admin("_edge")
self.admin_auth: str = auth_header(self.admin)
def test_delete_group_then_fallback_applies(self) -> None:
exp: User = _make_experimenter("_edge1")
appr: User = _make_approver("_edge1")
exp: User = make_experimenter("_edge1")
appr: User = make_approver("_edge1")
group: ApproverGroup = approver_group_create(
experimenter=exp,
approver_ids=[str(appr.pk)],
@@ -162,8 +165,8 @@ class ReviewEdgeCasesTest(TestCase):
self.assertEqual(min_app, 3)
def test_inactive_approver_excluded_from_effective_policy(self) -> None:
exp: User = _make_experimenter("_edge2")
appr: User = _make_approver("_edge2")
exp: User = make_experimenter("_edge2")
appr: User = make_approver("_edge2")
approver_group_create(
experimenter=exp,
approver_ids=[str(appr.pk)],
@@ -174,8 +177,8 @@ class ReviewEdgeCasesTest(TestCase):
self.assertEqual(approvers.count(), 0)
def test_create_group_with_all_three_approvers(self) -> None:
exp: User = _make_experimenter("_edge3")
apprs: list[User] = [_make_approver(f"_edge3_{i}") for i in range(3)]
exp: User = make_experimenter("_edge3")
apprs: list[User] = [make_approver(f"_edge3_{i}") for i in range(3)]
group: ApproverGroup = approver_group_create(
experimenter=exp,
approver_ids=[str(a.pk) for a in apprs],
@@ -185,8 +188,8 @@ class ReviewEdgeCasesTest(TestCase):
self.assertEqual(group.min_approvals, 2)
def test_update_group_to_empty_approvers(self) -> None:
exp: User = _make_experimenter("_edge4")
appr: User = _make_approver("_edge4")
exp: User = make_experimenter("_edge4")
appr: User = make_approver("_edge4")
group: ApproverGroup = approver_group_create(
experimenter=exp,
approver_ids=[str(appr.pk)],
@@ -198,15 +201,15 @@ class ReviewEdgeCasesTest(TestCase):
)
def test_api_output_format_approver_group(self) -> None:
exp: User = _make_experimenter("_edge5")
appr: User = _make_approver("_edge5")
exp: User = make_experimenter("_edge5")
appr: User = make_approver("_edge5")
resp = self.client.post(
reverse("api-1:create_approver_group"),
data=json.dumps(
{
"experimenterId": str(exp.pk),
"approverIds": [str(appr.pk)],
"minApprovals": 1,
"experimenter_id": str(exp.pk),
"approver_ids": [str(appr.pk)],
"min_approvals": 1,
}
),
content_type="application/json",
@@ -217,9 +220,9 @@ class ReviewEdgeCasesTest(TestCase):
self.assertIn("id", data)
self.assertIn("experimenter", data)
self.assertIn("approvers", data)
self.assertTrue("minApprovals" in data or "min_approvals" in data)
self.assertTrue("createdAt" in data or "created_at" in data)
self.assertTrue("updatedAt" in data or "updated_at" in data)
self.assertIn("min_approvals", data)
self.assertIn("created_at", data)
self.assertIn("updated_at", data)
self.assertIn("id", data["experimenter"])
self.assertIn("username", data["experimenter"])
self.assertIn("email", data["experimenter"])
@@ -237,17 +240,13 @@ class ReviewEdgeCasesTest(TestCase):
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertIn("id", data)
self.assertTrue(
"defaultMinApprovals" in data or "default_min_approvals" in data
)
self.assertTrue(
"allowAnyApprover" in data or "allow_any_approver" in data
)
self.assertTrue("updatedAt" in data or "updated_at" in data)
self.assertIn("default_min_approvals", data)
self.assertIn("allow_any_approver", data)
self.assertIn("updated_at", data)
def test_api_output_format_effective_policy(self) -> None:
exp: User = _make_experimenter("_edge6")
appr: User = _make_approver("_edge6")
exp: User = make_experimenter("_edge6")
appr: User = make_approver("_edge6")
approver_group_create(
experimenter=exp,
approver_ids=[str(appr.pk)],
@@ -261,19 +260,17 @@ class ReviewEdgeCasesTest(TestCase):
)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertTrue("experimenterId" in data or "experimenter_id" in data)
self.assertTrue("minApprovals" in data or "min_approvals" in data)
self.assertIn("experimenter_id", data)
self.assertIn("min_approvals", data)
self.assertIn("approvers", data)
self.assertIn("source", data)
self.assertTrue(
"hasExplicitGroup" in data or "has_explicit_group" in data
)
self.assertIn("has_explicit_group", data)
def test_multiple_experimenters_independent_groups(self) -> None:
exp1: User = _make_experimenter("_edge_m1")
exp2: User = _make_experimenter("_edge_m2")
appr1: User = _make_approver("_edge_m1")
appr2: User = _make_approver("_edge_m2")
exp1: User = make_experimenter("_edge_m1")
exp2: User = make_experimenter("_edge_m2")
appr1: User = make_approver("_edge_m1")
appr2: User = make_approver("_edge_m2")
g1: ApproverGroup = approver_group_create(
experimenter=exp1,
@@ -293,9 +290,9 @@ class ReviewEdgeCasesTest(TestCase):
self.assertFalse(g2.can_approve(appr1))
def test_concurrent_fallback_and_explicit_group(self) -> None:
exp_with: User = _make_experimenter("_edge_c1")
exp_without: User = _make_experimenter("_edge_c2")
appr: User = _make_approver("_edge_c")
exp_with: User = make_experimenter("_edge_c1")
exp_without: User = make_experimenter("_edge_c2")
appr: User = make_approver("_edge_c")
approver_group_create(
experimenter=exp_with,
+7 -1
View File
@@ -8,6 +8,7 @@ from ninja.renderers import BaseRenderer
from api.v1 import handlers
from api.v1.auth.endpoints import router as auth_router
from api.v1.flags.endpoints import router as flags_router
from api.v1.reviews.endpoints import router as reviews_router
from api.v1.users.endpoints import router as users_router
@@ -23,7 +24,7 @@ class ORJSONRenderer(BaseRenderer):
def default(self, obj: Any) -> Any:
if isinstance(obj, Schema):
return obj.model_dump(by_alias=True)
return obj.model_dump()
raise TypeError
@@ -41,6 +42,11 @@ router.add_router(
auth_router,
)
router.add_router(
"flags",
flags_router,
)
router.add_router(
"users",
users_router,
+3 -3
View File
@@ -12,18 +12,18 @@ class FieldError(Schema):
)
issue: str = Field(..., description="Problem description")
rejected_value: Any = Field(
None, alias="rejectedValue", description="Value that failed validation"
None, description="Value that failed validation"
)
class ApiError(Schema):
code: str
message: str
trace_id: str = Field(..., alias="traceId")
trace_id: str
timestamp: datetime
path: str
details: dict[str, Any] | None = None
class ValidationError(ApiError):
field_errors: list[FieldError] = Field(..., alias="fieldErrors")
field_errors: list[FieldError]
+1 -1
View File
@@ -38,7 +38,7 @@ router = Router(tags=["users"])
def list_users(
request: HttpRequest,
role: str | None = None,
is_active: bool | None = None,
is_active: bool | None = None, # noqa: FBT001
search: str | None = None,
limit: int = 50,
offset: int = 0,
+1 -7
View File
@@ -7,8 +7,7 @@ from apps.users.models import User
class UserOut(ModelSchema):
first_name: str = Field("", alias="firstName")
last_name: str = Field("", alias="lastName")
is_active: bool
class Meta:
@@ -52,13 +51,11 @@ class UserCreateIn(Schema):
)
first_name: str = Field(
"",
alias="firstName",
max_length=150,
description="First name.",
)
last_name: str = Field(
"",
alias="lastName",
max_length=150,
description="Last name.",
)
@@ -91,19 +88,16 @@ class UserUpdateIn(Schema):
)
first_name: str | None = Field(
None,
alias="firstName",
max_length=150,
description="New first name.",
)
last_name: str | None = Field(
None,
alias="lastName",
max_length=150,
description="New last name.",
)
is_active: bool | None = Field(
None,
alias="isActive",
description="Set active/inactive status.",
)
+8 -5
View File
@@ -1,23 +1,26 @@
from typing import override
from django.test import Client, TestCase
from apps.users.models import User, UserRole
from apps.users.tests._helpers import _auth_header, _make_user
from apps.users.tests.helpers import auth_header, make_user
class BaseUsersAPITest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.admin: User = _make_user(
self.admin: User = make_user(
username="mgmt_admin",
email="mgmt_admin@x.com",
password="adminpass1",
role=UserRole.ADMIN,
)
self.viewer: User = _make_user(
self.viewer: User = make_user(
username="mgmt_viewer",
email="mgmt_viewer@x.com",
password="viewerpass",
role=UserRole.VIEWER,
)
self.admin_auth: str = _auth_header(self.admin)
self.viewer_auth: str = _auth_header(self.viewer)
self.admin_auth: str = auth_header(self.admin)
self.viewer_auth: str = auth_header(self.viewer)
@@ -4,14 +4,14 @@ import uuid
from django.urls import reverse
from apps.users.models import User, UserRole
from apps.users.tests._helpers import _make_user
from apps.users.tests.helpers import make_user
from ._crud_base import BaseUsersAPITest
class UsersAPIDeleteAssignRoleTest(BaseUsersAPITest):
def test_delete_user_admin(self) -> None:
target: User = _make_user(
target: User = make_user(
username="to_delete",
email="del@lotty.local",
role=UserRole.VIEWER,
@@ -66,8 +66,8 @@ class UsersAPIListCreateTest(BaseUsersAPITest):
"email": "new@lotty.local",
"password": "newpass123",
"role": "experimenter",
"firstName": "New",
"lastName": "User",
"first_name": "New",
"last_name": "User",
}
),
content_type="application/json",
@@ -51,7 +51,7 @@ class UsersAPIReadUpdateTest(BaseUsersAPITest):
reverse(
"api-1:update_user", kwargs={"user_id": str(self.viewer.pk)}
),
data=json.dumps({"firstName": "Updated"}),
data=json.dumps({"first_name": "Updated"}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
@@ -75,7 +75,7 @@ class UsersAPIReadUpdateTest(BaseUsersAPITest):
reverse(
"api-1:update_user", kwargs={"user_id": str(self.admin.pk)}
),
data=json.dumps({"firstName": "Hacked"}),
data=json.dumps({"first_name": "Hacked"}),
content_type="application/json",
HTTP_AUTHORIZATION=self.viewer_auth,
)
@@ -86,7 +86,7 @@ class UsersAPIReadUpdateTest(BaseUsersAPITest):
reverse(
"api-1:update_user", kwargs={"user_id": str(uuid.uuid4())}
),
data=json.dumps({"firstName": "Ghost"}),
data=json.dumps({"first_name": "Ghost"}),
content_type="application/json",
HTTP_AUTHORIZATION=self.admin_auth,
)
+6 -4
View File
@@ -1,13 +1,15 @@
import json
from typing import override
from django.test import Client, TestCase
from django.urls import reverse
from apps.users.models import User, UserRole
from apps.users.tests._helpers import _auth_header, _make_user
from apps.users.tests.helpers import auth_header, make_user
class RoleBasedAccessControlTest(TestCase):
@override
def setUp(self) -> None:
self.client = Client()
self.roles = {}
@@ -17,7 +19,7 @@ class RoleBasedAccessControlTest(TestCase):
UserRole.APPROVER,
UserRole.VIEWER,
]:
user: User = _make_user(
user: User = make_user(
username=f"rbac_{role_val}",
email=f"rbac_{role_val}@x.com",
password="password1",
@@ -25,7 +27,7 @@ class RoleBasedAccessControlTest(TestCase):
)
self.roles[role_val] = {
"user": user,
"auth": _auth_header(user),
"auth": auth_header(user),
}
def test_admin_can_list(self) -> None:
@@ -93,7 +95,7 @@ class RoleBasedAccessControlTest(TestCase):
target = self.roles[UserRole.VIEWER]["user"]
resp = self.client.patch(
reverse("api-1:update_user", kwargs={"user_id": str(target.pk)}),
data=json.dumps({"firstName": "Nope"}),
data=json.dumps({"first_name": "Nope"}),
content_type="application/json",
HTTP_AUTHORIZATION=self.roles[UserRole.EXPERIMENTER]["auth"],
)