Skip to content

Commit 270eaec

Browse files
authored
add proper response codes to attach endpoint (#3084)
1 parent d66530f commit 270eaec

File tree

6 files changed

+56
-15
lines changed

6 files changed

+56
-15
lines changed

b2b/views/v0/__init__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ def post(self, request, enrollment_code: str, format=None): # noqa: A002, ARG00
117117
If the user is already in the contract, then we skip it.
118118
119119
Returns:
120+
- 201: Code successfully redeemed and user attached to new contract(s)
121+
- 200: Code valid but user already attached to all associated contracts
122+
- 404: Invalid or expired enrollment code
120123
- list of ContractPageSerializer - the contracts for the user
121124
"""
122125

@@ -141,9 +144,8 @@ def get_active_user_contracts(user):
141144
)
142145
except Discount.DoesNotExist:
143146
return Response(
144-
ContractPageSerializer(
145-
get_active_user_contracts(request.user), many=True
146-
).data
147+
{"detail": "Invalid or expired enrollment code."},
148+
status=status.HTTP_404_NOT_FOUND,
147149
)
148150

149151
contract_ids = list(code.b2b_contracts().values_list("id", flat=True))
@@ -154,6 +156,7 @@ def get_active_user_contracts(user):
154156
.all()
155157
)
156158

159+
contracts_attached = False
157160
for contract in contracts:
158161
if contract.is_full():
159162
continue
@@ -165,11 +168,17 @@ def get_active_user_contracts(user):
165168
DiscountContractAttachmentRedemption.objects.create(
166169
user=request.user, discount=code, contract=contract
167170
)
171+
contracts_attached = True
168172

169173
request.user.save()
170174

175+
response_status = (
176+
status.HTTP_201_CREATED if contracts_attached else status.HTTP_200_OK
177+
)
178+
171179
return Response(
172180
ContractPageSerializer(
173181
get_active_user_contracts(request.user), many=True
174-
).data
182+
).data,
183+
status=response_status,
175184
)

b2b/views/v0/views_test.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def test_b2b_contract_attachment_bad_code(user):
3535
url = reverse("b2b:attach-user", kwargs={"enrollment_code": "not a code"})
3636
resp = client.post(url)
3737

38-
assert resp.status_code == 200
38+
assert resp.status_code == 404
39+
assert resp.json()["detail"] == "Invalid or expired enrollment code."
3940
assert user.b2b_contracts.count() == 0
4041

4142

@@ -90,18 +91,21 @@ def test_b2b_contract_attachment(mocker, max_learners, code_used):
9091
)
9192
resp = client.post(url)
9293

93-
assert resp.status_code == 200
94-
9594
user.refresh_from_db()
9695

9796
if code_used and max_learners:
97+
# Code already used for attachment and is not unlimited - should return 404
98+
assert resp.status_code == 404
99+
assert resp.json()["detail"] == "Invalid or expired enrollment code."
98100
assert not user.b2b_organizations.filter(pk=contract.organization.id).exists()
99101
assert not user.b2b_contracts.filter(pk=contract.id).exists()
100102

101103
assert not DiscountContractAttachmentRedemption.objects.filter(
102104
contract=contract, user=user, discount=contract_codes[0]
103105
).exists()
104106
else:
107+
# Successfully attached to contract - should return 201
108+
assert resp.status_code == 201
105109
mocked_attach_user.assert_called()
106110

107111
assert user.b2b_organizations.filter(pk=contract.organization.id).exists()
@@ -157,7 +161,9 @@ def test_b2b_contract_attachment_invalid_code_dates(user, bad_start_or_end):
157161
with freeze_time(slightly_future_time):
158162
resp = client.post(url)
159163

160-
assert resp.status_code == 200
164+
# Code is expired/not yet active - should return 404
165+
assert resp.status_code == 404
166+
assert resp.json()["detail"] == "Invalid or expired enrollment code."
161167

162168
user.refresh_from_db()
163169
assert not user.b2b_contracts.filter(pk=contract.id).exists()
@@ -211,6 +217,7 @@ def test_b2b_contract_attachment_invalid_contract_dates(user, bad_start_or_end):
211217
with freeze_time(slightly_future_time):
212218
resp = client.post(url)
213219

220+
# Contract dates are invalid - code is valid but no contracts to attach - should return 200
214221
assert resp.status_code == 200
215222

216223
user.refresh_from_db()
@@ -248,7 +255,8 @@ def test_b2b_contract_attachment_full_contract(mocker):
248255
)
249256
resp = client.post(url)
250257

251-
assert resp.status_code == 200
258+
# Successfully attached - should return 201
259+
assert resp.status_code == 201
252260
mocked_attach_user.assert_called()
253261

254262
user.refresh_from_db()
@@ -265,7 +273,9 @@ def test_b2b_contract_attachment_full_contract(mocker):
265273
)
266274
resp = client.post(url)
267275

268-
assert resp.status_code == 200
276+
# Code already used for attachment and contract has max_learners=1 - should return 404
277+
assert resp.status_code == 404
278+
assert resp.json()["detail"] == "Invalid or expired enrollment code."
269279
mocked_attach_user.assert_not_called()
270280

271281
user.refresh_from_db()

courses/conftest.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ def course_catalog_data(course_catalog_program_count, course_catalog_course_coun
6060
course_catalog_course_count(int): number of courses to generate.
6161
course_catalog_program_count(int): number of programs to generate.
6262
"""
63-
# Use a local Random instance with fixed seed for deterministic test data
63+
import random
64+
65+
# Seed both local and global random for full determinism
66+
# Factory Boy's FuzzyText uses the global random module
67+
random.seed(42)
6468
rng = Random(42) # noqa: S311
6569
programs = []
6670
courses = []
@@ -99,13 +103,22 @@ def _create_program(courses, rng):
99103
elective_flag=True,
100104
)
101105
if len(courses) > 3: # noqa: PLR2004
102-
for c in rng.sample(courses, 3):
106+
# Use deterministic indices instead of random sampling
107+
# This ensures the same courses are selected regardless of test execution order
108+
indices = list(range(len(courses)))
109+
rng.shuffle(indices)
110+
111+
# Select 3 courses for required
112+
for idx in indices[:3]:
103113
required_courses_node.add_child(
104-
node_type=ProgramRequirementNodeType.COURSE, course=c
114+
node_type=ProgramRequirementNodeType.COURSE, course=courses[idx]
105115
)
106-
for c in rng.sample(courses, 3):
116+
117+
# Select 3 courses for electives (reuse the shuffled indices)
118+
elective_indices = indices[3:6] if len(indices) >= 6 else indices[:3] # noqa: PLR2004
119+
for idx in elective_indices:
107120
elective_courses_node.add_child(
108-
node_type=ProgramRequirementNodeType.COURSE, course=c
121+
node_type=ProgramRequirementNodeType.COURSE, course=courses[idx]
109122
)
110123
else:
111124
required_courses_node.add_child(

openapi/specs/v0.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ paths:
117117
If the user is already in the contract, then we skip it.
118118
119119
Returns:
120+
- 201: Code successfully redeemed and user attached to new contract(s)
121+
- 200: Code valid but user already attached to all associated contracts
122+
- 404: Invalid or expired enrollment code
120123
- list of ContractPageSerializer - the contracts for the user
121124
parameters:
122125
- in: path

openapi/specs/v1.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ paths:
117117
If the user is already in the contract, then we skip it.
118118
119119
Returns:
120+
- 201: Code successfully redeemed and user attached to new contract(s)
121+
- 200: Code valid but user already attached to all associated contracts
122+
- 404: Invalid or expired enrollment code
120123
- list of ContractPageSerializer - the contracts for the user
121124
parameters:
122125
- in: path

openapi/specs/v2.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ paths:
117117
If the user is already in the contract, then we skip it.
118118
119119
Returns:
120+
- 201: Code successfully redeemed and user attached to new contract(s)
121+
- 200: Code valid but user already attached to all associated contracts
122+
- 404: Invalid or expired enrollment code
120123
- list of ContractPageSerializer - the contracts for the user
121124
parameters:
122125
- in: path

0 commit comments

Comments
 (0)