Skip to content

Commit 04f6ccc

Browse files
committed
Add tests to test the whole flow
Tests the device flow end to end
1 parent 0675f52 commit 04f6ccc

File tree

1 file changed

+115
-1
lines changed

1 file changed

+115
-1
lines changed

tests/test_device.py

+115-1
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,17 @@ def setUpTestData(cls):
3434
name="test_client_credentials_app",
3535
user=cls.dev_user,
3636
client_type=Application.CLIENT_PUBLIC,
37-
authorization_grant_type=Application.GRANT_CLIENT_CREDENTIALS,
37+
authorization_grant_type=Application.GRANT_DEVICE_CODE,
3838
client_secret="abcdefghijklmnopqrstuvwxyz1234567890",
3939
)
4040

4141

4242
class TestDeviceFlow(BaseTest):
43+
"""
44+
The first 2 tests test the device flow in order
45+
how the device flow works
46+
"""
47+
4348
@mock.patch(
4449
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
4550
lambda: "abc",
@@ -96,6 +101,115 @@ def test_device_flow_authorization_initiation(self):
96101
"interval": 5,
97102
}
98103

104+
@mock.patch(
105+
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
106+
lambda: "abc",
107+
)
108+
def test_device_flow_authorization_user_code_confirm_and_access_token(self):
109+
"""
110+
1. User visits the /device endpoint in their browsers and submits the user code
111+
112+
the device and approve deny actions occur concurrently
113+
(i.e the device is polling the token endpoint while the user
114+
either approves or denies the device)
115+
116+
-2(3)-. User approves or denies the device
117+
-3(2)-. Device polls the /token endpoint
118+
"""
119+
120+
# -----------------------
121+
# 0: Setup device flow
122+
# -----------------------
123+
self.oauth2_settings.OAUTH_DEVICE_VERIFICATION_URI = "example.com/device"
124+
self.oauth2_settings.OAUTH_DEVICE_USER_CODE_GENERATOR = lambda: "xyz"
125+
126+
request_data: dict[str, str] = {
127+
"client_id": self.application.client_id,
128+
}
129+
request_as_x_www_form_urlencoded: str = urlencode(request_data)
130+
131+
django.http.response.JsonResponse = self.client.post(
132+
reverse("oauth2_provider:device-authorization"),
133+
data=request_as_x_www_form_urlencoded,
134+
content_type="application/x-www-form-urlencoded",
135+
)
136+
137+
# /device and /device_confirm require a user to be logged in
138+
# to access it
139+
UserModel.objects.create_user(
140+
username="test_user_device_flow",
141+
142+
password="password123",
143+
)
144+
self.client.login(username="test_user_device_flow", password="password123")
145+
146+
# --------------------------------------------------------------------------------
147+
# 1. User visits the /device endpoint in their browsers and submits the user code
148+
# submits wrong code then right code
149+
# --------------------------------------------------------------------------------
150+
151+
# 1. User visits the /device endpoint in their browsers and submits the user code
152+
# (GET Request to load it)
153+
get_response = self.client.get(reverse("oauth2_provider:device"))
154+
assert get_response.status_code == 200
155+
assert "form" in get_response.context # Ensure the form is rendered in the context
156+
157+
# 1.1.0 User visits the /device endpoint in their browsers and submits wrong user code
158+
with pytest.raises(oauth2_provider.models.Device.DoesNotExist):
159+
self.client.post(
160+
reverse("oauth2_provider:device"),
161+
data={"user_code": "invalid_code"},
162+
)
163+
164+
# 1.1.1: user submits valid user code
165+
post_response_valid = self.client.post(
166+
reverse("oauth2_provider:device"),
167+
data={"user_code": "xyz"},
168+
)
169+
170+
device_confirm_url = reverse("oauth2_provider:device-confirm", kwargs={"device_code": "abc"})
171+
assert post_response_valid.status_code == 308 # Ensure it redirects with 308 status
172+
assert post_response_valid["Location"] == device_confirm_url
173+
174+
device_confirm_url = reverse("oauth2_provider:device-confirm", kwargs={"device_code": "abc"})
175+
assert post_response_valid["Location"] == device_confirm_url
176+
177+
# --------------------------------------------------------------------------------
178+
# 2: We redirect to the accept/deny form (the user is still in their browser)
179+
# and approves
180+
# --------------------------------------------------------------------------------
181+
get_confirm = self.client.get(device_confirm_url)
182+
assert get_confirm.status_code == 200
183+
184+
approve_response = self.client.post(device_confirm_url, data={"action": "accept"})
185+
assert approve_response.status_code == 200
186+
assert approve_response.content.decode() == "approved"
187+
188+
device = DeviceModel.objects.get(device_code="abc")
189+
assert device.status == device.AUTHORIZED
190+
191+
# -------------------------
192+
# 3: Device polls /token
193+
# -------------------------
194+
token_payload = {
195+
"device_code": device.device_code,
196+
"client_id": self.application.client_id,
197+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
198+
}
199+
token_response = self.client.post(
200+
reverse("oauth2_provider:token"),
201+
data=urlencode(token_payload),
202+
content_type="application/x-www-form-urlencoded",
203+
)
204+
205+
assert token_response.status_code == 200
206+
207+
token_data = token_response.json()
208+
209+
assert "access_token" in token_data
210+
assert token_data["token_type"].lower() == "bearer"
211+
assert "scope" in token_data
212+
99213
@mock.patch(
100214
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
101215
lambda: "abc",

0 commit comments

Comments
 (0)