Skip to content

Commit b63d7e0

Browse files
committed
Add tests to test the whole flow
Tests the device flow end to end
1 parent af11d0e commit b63d7e0

File tree

1 file changed

+107
-1
lines changed

1 file changed

+107
-1
lines changed

tests/test_device.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ 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

@@ -101,6 +101,112 @@ def test_device_flow_authorization_initiation(self):
101101
"interval": 5,
102102
}
103103

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+
# --------------------------------------------------------------------------------
175+
# 2: We redirect to the accept/deny form (the user is still in their browser)
176+
# and approves
177+
# --------------------------------------------------------------------------------
178+
get_confirm = self.client.get(device_confirm_url)
179+
assert get_confirm.status_code == 200
180+
181+
approve_response = self.client.post(device_confirm_url, data={"action": "accept"})
182+
assert approve_response.status_code == 200
183+
assert approve_response.content.decode() == "approved"
184+
185+
device = DeviceModel.objects.get(device_code="abc")
186+
assert device.status == device.AUTHORIZED
187+
188+
# -------------------------
189+
# 3: Device polls /token
190+
# -------------------------
191+
token_payload = {
192+
"device_code": device.device_code,
193+
"client_id": self.application.client_id,
194+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
195+
}
196+
token_response = self.client.post(
197+
"/o/token/",
198+
data=urlencode(token_payload),
199+
content_type="application/x-www-form-urlencoded",
200+
)
201+
202+
assert token_response.status_code == 200
203+
204+
token_data = token_response.json()
205+
206+
assert "access_token" in token_data
207+
assert token_data["token_type"].lower() == "bearer"
208+
assert "scope" in token_data
209+
104210
@mock.patch(
105211
"oauthlib.oauth2.rfc8628.endpoints.device_authorization.generate_token",
106212
lambda: "abc",

0 commit comments

Comments
 (0)