cornflow 1.1.5a1__py3-none-any.whl → 1.2.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cornflow/app.py +8 -3
- cornflow/cli/service.py +14 -6
- cornflow/config.py +9 -10
- cornflow/endpoints/login.py +54 -57
- cornflow/schemas/user.py +3 -17
- cornflow/shared/authentication/auth.py +211 -254
- cornflow/shared/const.py +3 -14
- cornflow/tests/custom_test_case.py +42 -21
- cornflow/tests/unit/test_actions.py +2 -2
- cornflow/tests/unit/test_apiview.py +2 -2
- cornflow/tests/unit/test_cases.py +20 -29
- cornflow/tests/unit/test_dags.py +5 -5
- cornflow/tests/unit/test_instances.py +2 -2
- cornflow/tests/unit/test_instances_file.py +1 -1
- cornflow/tests/unit/test_licenses.py +1 -1
- cornflow/tests/unit/test_log_in.py +227 -207
- cornflow/tests/unit/test_permissions.py +8 -8
- cornflow/tests/unit/test_roles.py +10 -10
- cornflow/tests/unit/test_tables.py +7 -7
- cornflow/tests/unit/test_token.py +12 -6
- {cornflow-1.1.5a1.dist-info → cornflow-1.2.0a1.dist-info}/METADATA +3 -2
- {cornflow-1.1.5a1.dist-info → cornflow-1.2.0a1.dist-info}/RECORD +25 -25
- {cornflow-1.1.5a1.dist-info → cornflow-1.2.0a1.dist-info}/WHEEL +1 -1
- {cornflow-1.1.5a1.dist-info → cornflow-1.2.0a1.dist-info}/entry_points.txt +0 -0
- {cornflow-1.1.5a1.dist-info → cornflow-1.2.0a1.dist-info}/top_level.txt +0 -0
@@ -5,6 +5,9 @@ Unit test for the log in endpoint
|
|
5
5
|
import json
|
6
6
|
import logging as log
|
7
7
|
from unittest import mock
|
8
|
+
import requests
|
9
|
+
import jwt
|
10
|
+
from datetime import datetime, timedelta
|
8
11
|
|
9
12
|
from flask import current_app
|
10
13
|
|
@@ -14,7 +17,7 @@ from cornflow.commands.access import access_init_command
|
|
14
17
|
from cornflow.commands.dag import register_deployed_dags_command_test
|
15
18
|
from cornflow.models import UserModel
|
16
19
|
from cornflow.shared import db
|
17
|
-
from cornflow.shared.const import SERVICE_ROLE,
|
20
|
+
from cornflow.shared.const import SERVICE_ROLE, INTERNAL_TOKEN_ISSUER, AUTH_OID
|
18
21
|
from cornflow.tests.const import LOGIN_URL
|
19
22
|
from cornflow.tests.custom_test_case import CustomTestCase, LoginTestCases
|
20
23
|
|
@@ -65,7 +68,7 @@ class TestLogIn(LoginTestCases.LoginEndpoint):
|
|
65
68
|
self.assertIn("Error in generating user token", response.json["error"])
|
66
69
|
|
67
70
|
|
68
|
-
class
|
71
|
+
class TestLogInOpenAuth(CustomTestCase):
|
69
72
|
def create_app(self):
|
70
73
|
"""
|
71
74
|
Creates and configures a Flask application for testing.
|
@@ -92,9 +95,9 @@ class TestLogInOpenAuthNoConfig(CustomTestCase):
|
|
92
95
|
test_user.save()
|
93
96
|
|
94
97
|
self.user_data.pop("email")
|
95
|
-
|
96
98
|
self.test_user_id = test_user.id
|
97
99
|
|
100
|
+
# Setup service user
|
98
101
|
self.service_data = {
|
99
102
|
"username": "service_user",
|
100
103
|
"email": "service@test.com",
|
@@ -120,283 +123,291 @@ class TestLogInOpenAuthNoConfig(CustomTestCase):
|
|
120
123
|
)
|
121
124
|
|
122
125
|
self.assertEqual(400, response.status_code)
|
123
|
-
self.assertEqual(
|
124
|
-
|
125
|
-
|
126
|
-
"""
|
127
|
-
Tests that a user can not log in with username and password
|
128
|
-
"""
|
129
|
-
response = self.client.post(
|
130
|
-
LOGIN_URL,
|
131
|
-
data=json.dumps(self.user_data),
|
132
|
-
headers={"Content-Type": "application/json"},
|
126
|
+
self.assertEqual(
|
127
|
+
response.json["error"],
|
128
|
+
"Must provide a token in Authorization header. Cannot log in with username and password",
|
133
129
|
)
|
134
130
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
class TestLogInOpenAuthAzure(CustomTestCase):
|
140
|
-
def create_app(self):
|
131
|
+
@mock.patch("cornflow.shared.authentication.auth.Auth.get_public_keys")
|
132
|
+
@mock.patch("cornflow.shared.authentication.auth.jwt")
|
133
|
+
def test_kid_not_in_public_keys(self, mock_jwt, mock_get_public_keys):
|
141
134
|
"""
|
142
|
-
|
143
|
-
|
144
|
-
:returns: A configured Flask application instance
|
145
|
-
:rtype: Flask
|
135
|
+
Tests token validation failure when the kid is not found in public keys
|
146
136
|
"""
|
147
|
-
|
148
|
-
|
149
|
-
app.config["OID_PROVIDER"] = OID_AZURE
|
150
|
-
app.config["OID_CLIENT_ID"] = "SOME_SECRET"
|
151
|
-
app.config["OID_TENANT_ID"] = "SOME_SECRET"
|
152
|
-
app.config["OID_ISSUER"] = "SOME_SECRET"
|
153
|
-
return app
|
137
|
+
# Import the real exceptions to ensure they are preserved
|
138
|
+
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError
|
154
139
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
access_init_command(verbose=False)
|
159
|
-
register_deployed_dags_command_test(verbose=False)
|
140
|
+
# Keep the real exception classes in the mock
|
141
|
+
mock_jwt.ExpiredSignatureError = ExpiredSignatureError
|
142
|
+
mock_jwt.InvalidTokenError = InvalidTokenError
|
160
143
|
|
161
|
-
|
162
|
-
"username": "service_user",
|
163
|
-
"email": "service@test.com",
|
164
|
-
"password": "Testpassword1!",
|
165
|
-
}
|
144
|
+
mock_jwt.get_unverified_header.return_value = {"kid": "test_kid"}
|
166
145
|
|
167
|
-
|
168
|
-
|
146
|
+
# Mock jwt.decode to return different results based on arguments
|
147
|
+
def decode_side_effect(*args, **kwargs):
|
148
|
+
if kwargs.get("options", {}).get("verify_signature") is False:
|
149
|
+
return {"iss": current_app.config.get("OID_PROVIDER", "valid_issuer")}
|
150
|
+
raise InvalidTokenError("Invalid token")
|
169
151
|
|
170
|
-
|
171
|
-
self.service_user_id = service_user.id
|
152
|
+
mock_jwt.decode.side_effect = decode_side_effect
|
172
153
|
|
173
|
-
|
154
|
+
# Mock get_public_keys to return keys that don't contain our kid
|
155
|
+
mock_get_public_keys.return_value = {"different_kid": "some_key"}
|
174
156
|
|
175
|
-
def test_service_user_login_first_fail(self):
|
176
|
-
"""
|
177
|
-
Tests that a service user can not log in with username and password
|
178
|
-
"""
|
179
157
|
response = self.client.post(
|
180
158
|
LOGIN_URL,
|
181
|
-
data=json.dumps({
|
182
|
-
headers={
|
159
|
+
data=json.dumps({}),
|
160
|
+
headers={
|
161
|
+
"Content-Type": "application/json",
|
162
|
+
"Authorization": "Bearer some_token",
|
163
|
+
},
|
183
164
|
)
|
184
165
|
|
185
166
|
self.assertEqual(400, response.status_code)
|
186
|
-
self.assertEqual(
|
167
|
+
self.assertEqual(
|
168
|
+
response.json["error"], "Invalid token: Unknown key identifier (kid)"
|
169
|
+
)
|
187
170
|
|
188
171
|
@mock.patch("cornflow.shared.authentication.auth.jwt")
|
189
|
-
def
|
172
|
+
def test_missing_kid_in_token(self, mock_jwt):
|
190
173
|
"""
|
191
|
-
Tests
|
174
|
+
Tests token validation failure when the token header is missing the kid
|
192
175
|
"""
|
193
|
-
|
176
|
+
# Import the real exceptions to ensure they are preserved
|
177
|
+
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError
|
178
|
+
|
179
|
+
# Keep the real exception classes in the mock
|
180
|
+
mock_jwt.ExpiredSignatureError = ExpiredSignatureError
|
181
|
+
mock_jwt.InvalidTokenError = InvalidTokenError
|
182
|
+
|
183
|
+
# Mock jwt.get_unverified_header to return a header without kid
|
184
|
+
mock_jwt.get_unverified_header.return_value = {"alg": "RS256"}
|
185
|
+
|
194
186
|
response = self.client.post(
|
195
187
|
LOGIN_URL,
|
196
|
-
data=json.dumps({
|
197
|
-
headers={
|
188
|
+
data=json.dumps({}),
|
189
|
+
headers={
|
190
|
+
"Content-Type": "application/json",
|
191
|
+
"Authorization": "Bearer some_token",
|
192
|
+
},
|
198
193
|
)
|
199
194
|
|
200
195
|
self.assertEqual(400, response.status_code)
|
201
|
-
self.assertEqual(
|
196
|
+
self.assertEqual(
|
197
|
+
response.json["error"],
|
198
|
+
"Invalid token: Missing key identifier (kid) in token header",
|
199
|
+
)
|
202
200
|
|
201
|
+
@mock.patch("cornflow.shared.authentication.auth.requests.get")
|
203
202
|
@mock.patch("cornflow.shared.authentication.auth.jwt")
|
204
|
-
def
|
203
|
+
def test_public_keys_fetch_fail(self, mock_jwt, mock_get):
|
205
204
|
"""
|
206
|
-
Tests
|
205
|
+
Tests failure when trying to fetch public keys from the OIDC provider
|
207
206
|
"""
|
208
|
-
|
207
|
+
# Import the real exceptions to ensure they are preserved
|
208
|
+
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError
|
209
|
+
|
210
|
+
# Keep the real exception classes in the mock
|
211
|
+
mock_jwt.ExpiredSignatureError = ExpiredSignatureError
|
212
|
+
mock_jwt.InvalidTokenError = InvalidTokenError
|
213
|
+
|
214
|
+
# Clear the cache
|
215
|
+
from cornflow.shared.authentication.auth import public_keys_cache
|
216
|
+
|
217
|
+
public_keys_cache.clear()
|
218
|
+
|
219
|
+
# Mock jwt to pass initial validation
|
220
|
+
mock_jwt.get_unverified_header.return_value = {"kid": "test_kid"}
|
221
|
+
mock_jwt.decode.side_effect = lambda *args, **kwargs: (
|
222
|
+
{"iss": current_app.config["OID_PROVIDER"]}
|
223
|
+
if kwargs.get("options", {}).get("verify_signature") is False
|
224
|
+
else {}
|
225
|
+
)
|
226
|
+
|
227
|
+
# Mock get to fail
|
228
|
+
mock_get.side_effect = requests.exceptions.RequestException(
|
229
|
+
"Failed to get keys"
|
230
|
+
)
|
231
|
+
|
209
232
|
response = self.client.post(
|
210
233
|
LOGIN_URL,
|
211
|
-
data=json.dumps({
|
212
|
-
headers={
|
234
|
+
data=json.dumps({}),
|
235
|
+
headers={
|
236
|
+
"Content-Type": "application/json",
|
237
|
+
"Authorization": "Bearer some_token",
|
238
|
+
},
|
213
239
|
)
|
214
240
|
|
215
241
|
self.assertEqual(400, response.status_code)
|
216
|
-
self.
|
217
|
-
|
242
|
+
self.assertEqual(
|
243
|
+
response.json["error"],
|
244
|
+
"Failed to fetch public keys from authentication provider",
|
218
245
|
)
|
219
246
|
|
220
|
-
@mock.patch("cornflow.
|
221
|
-
def test_service_user_login_no_fail(self,
|
222
|
-
|
247
|
+
@mock.patch("cornflow.shared.authentication.Auth.decode_token")
|
248
|
+
def test_service_user_login_no_fail(self, mock_decode):
|
249
|
+
"""
|
250
|
+
Tests successful login for an existing service user with valid token
|
251
|
+
"""
|
252
|
+
mock_decode.return_value = {"username": "service_user"}
|
223
253
|
response = self.client.post(
|
224
254
|
LOGIN_URL,
|
225
|
-
data=json.dumps({
|
226
|
-
headers={
|
255
|
+
data=json.dumps({}),
|
256
|
+
headers={
|
257
|
+
"Content-Type": "application/json",
|
258
|
+
"Authorization": "Bearer some_token",
|
259
|
+
},
|
227
260
|
)
|
228
261
|
|
229
|
-
print(response.json)
|
230
|
-
|
231
262
|
self.assertEqual(200, response.status_code)
|
232
263
|
self.assertEqual(self.service_user_id, response.json["id"])
|
233
264
|
|
234
|
-
@mock.patch("cornflow.
|
235
|
-
def test_new_user_login_no_fail(self,
|
236
|
-
|
265
|
+
@mock.patch("cornflow.shared.authentication.Auth.decode_token")
|
266
|
+
def test_new_user_login_no_fail(self, mock_decode):
|
267
|
+
"""
|
268
|
+
Tests successful login and creation of a new user with valid token
|
269
|
+
"""
|
270
|
+
mock_decode.return_value = {"username": "test_user"}
|
237
271
|
response = self.client.post(
|
238
272
|
LOGIN_URL,
|
239
|
-
data=json.dumps({
|
240
|
-
headers={
|
273
|
+
data=json.dumps({}),
|
274
|
+
headers={
|
275
|
+
"Content-Type": "application/json",
|
276
|
+
"Authorization": "Bearer some_token",
|
277
|
+
},
|
241
278
|
)
|
242
279
|
|
243
280
|
self.assertEqual(200, response.status_code)
|
244
281
|
self.assertEqual(self.service_user_id + 1, response.json["id"])
|
245
282
|
|
246
|
-
|
247
|
-
|
248
|
-
def
|
283
|
+
@mock.patch("cornflow.shared.authentication.Auth.verify_token")
|
284
|
+
@mock.patch("cornflow.shared.authentication.auth.jwt")
|
285
|
+
def test_public_keys_caching(self, mock_jwt, mock_verify_token):
|
249
286
|
"""
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
:rtype: Flask
|
287
|
+
Tests that public keys are cached and reused for subsequent requests.
|
288
|
+
Also verifies that when a new kid is encountered that's not in the cache,
|
289
|
+
the system fetches fresh keys from the provider.
|
254
290
|
"""
|
255
|
-
|
256
|
-
|
257
|
-
app.config["OID_PROVIDER"] = OID_GOOGLE
|
258
|
-
app.config["OID_CLIENT_ID"] = "SOME_SECRET"
|
259
|
-
app.config["OID_TENANT_ID"] = "SOME_SECRET"
|
260
|
-
app.config["OID_ISSUER"] = "SOME_SECRET"
|
261
|
-
return app
|
291
|
+
# Import the real exceptions to ensure they are preserved
|
292
|
+
from jwt.exceptions import InvalidTokenError, ExpiredSignatureError
|
262
293
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
access_init_command(verbose=False)
|
267
|
-
register_deployed_dags_command_test(verbose=False)
|
294
|
+
# Keep the real exception classes in the mock
|
295
|
+
mock_jwt.ExpiredSignatureError = ExpiredSignatureError
|
296
|
+
mock_jwt.InvalidTokenError = InvalidTokenError
|
268
297
|
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
"
|
273
|
-
|
298
|
+
# Mock jwt to return valid unverified header and payload
|
299
|
+
mock_jwt.get_unverified_header.return_value = {"kid": "test_kid"}
|
300
|
+
mock_jwt.decode.side_effect = lambda *args, **kwargs: (
|
301
|
+
{"iss": current_app.config["OID_PROVIDER"]}
|
302
|
+
if kwargs.get("options", {}).get("verify_signature") is False
|
303
|
+
else {"sub": "test_user", "email": "test@test.com"}
|
304
|
+
)
|
274
305
|
|
275
|
-
|
276
|
-
|
306
|
+
# Mock verify_token to always return a valid token payload
|
307
|
+
mock_verify_token.return_value = {"sub": "test_user", "email": "test@test.com"}
|
277
308
|
|
278
|
-
|
279
|
-
|
309
|
+
# Make first request
|
310
|
+
response = self.client.post(
|
311
|
+
LOGIN_URL,
|
312
|
+
data=json.dumps({}),
|
313
|
+
headers={
|
314
|
+
"Content-Type": "application/json",
|
315
|
+
"Authorization": "Bearer some_token",
|
316
|
+
},
|
317
|
+
)
|
280
318
|
|
281
|
-
self.
|
319
|
+
self.assertEqual(200, response.status_code)
|
282
320
|
|
283
|
-
|
284
|
-
"""
|
285
|
-
Tests that a service user can not log in with username and password
|
286
|
-
"""
|
321
|
+
# Make second request
|
287
322
|
response = self.client.post(
|
288
323
|
LOGIN_URL,
|
289
|
-
data=json.dumps({
|
290
|
-
headers={
|
324
|
+
data=json.dumps({}),
|
325
|
+
headers={
|
326
|
+
"Content-Type": "application/json",
|
327
|
+
"Authorization": "Bearer some_token",
|
328
|
+
},
|
291
329
|
)
|
292
330
|
|
293
|
-
self.assertEqual(
|
294
|
-
self.assertEqual(
|
295
|
-
response.json["error"], "The selected OID provider is not implemented"
|
296
|
-
)
|
331
|
+
self.assertEqual(200, response.status_code)
|
297
332
|
|
333
|
+
# Verify token was verified twice
|
334
|
+
self.assertEqual(2, mock_verify_token.call_count)
|
298
335
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
336
|
+
# Now test with a different token
|
337
|
+
response = self.client.post(
|
338
|
+
LOGIN_URL,
|
339
|
+
data=json.dumps({}),
|
340
|
+
headers={
|
341
|
+
"Content-Type": "application/json",
|
342
|
+
"Authorization": "Bearer some_different_token",
|
343
|
+
},
|
344
|
+
)
|
303
345
|
|
304
|
-
|
305
|
-
:rtype: Flask
|
306
|
-
"""
|
307
|
-
app = create_app("testing-oauth")
|
308
|
-
app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] = 0
|
309
|
-
app.config["OID_PROVIDER"] = OID_NONE
|
310
|
-
app.config["OID_CLIENT_ID"] = "SOME_SECRET"
|
311
|
-
app.config["OID_TENANT_ID"] = "SOME_SECRET"
|
312
|
-
app.config["OID_ISSUER"] = "SOME_SECRET"
|
313
|
-
return app
|
346
|
+
self.assertEqual(200, response.status_code)
|
314
347
|
|
315
|
-
|
316
|
-
|
317
|
-
db.create_all()
|
318
|
-
access_init_command(verbose=False)
|
319
|
-
register_deployed_dags_command_test(verbose=False)
|
348
|
+
# Verify token was verified a third time
|
349
|
+
self.assertEqual(3, mock_verify_token.call_count)
|
320
350
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
351
|
+
def test_old_token(self):
|
352
|
+
"""
|
353
|
+
Tests using an expired token.
|
354
|
+
"""
|
355
|
+
# Generate an expired token
|
356
|
+
payload = {
|
357
|
+
# Token expired 1 hour ago
|
358
|
+
"exp": datetime.utcnow() - timedelta(hours=1),
|
359
|
+
# Token created 2 hours ago
|
360
|
+
"iat": datetime.utcnow() - timedelta(hours=2),
|
361
|
+
"sub": "testname",
|
362
|
+
"iss": INTERNAL_TOKEN_ISSUER,
|
325
363
|
}
|
326
364
|
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
self.service_data.pop("email")
|
331
|
-
self.service_user_id = service_user.id
|
332
|
-
|
333
|
-
self.assign_role(self.service_user_id, SERVICE_ROLE)
|
365
|
+
expired_token = jwt.encode(
|
366
|
+
payload, current_app.config["SECRET_TOKEN_KEY"], algorithm="HS256"
|
367
|
+
)
|
334
368
|
|
335
|
-
def test_service_user_login(self):
|
336
|
-
"""
|
337
|
-
Tests that a service user can not log in with username and password
|
338
|
-
"""
|
339
369
|
response = self.client.post(
|
340
370
|
LOGIN_URL,
|
341
|
-
data=json.dumps({
|
342
|
-
headers={
|
371
|
+
data=json.dumps({}),
|
372
|
+
headers={
|
373
|
+
"Content-Type": "application/json",
|
374
|
+
"Authorization": f"Bearer {expired_token}",
|
375
|
+
},
|
343
376
|
)
|
344
377
|
|
345
|
-
self.assertEqual(
|
378
|
+
self.assertEqual(400, response.status_code)
|
346
379
|
self.assertEqual(
|
347
|
-
|
380
|
+
"The token has expired, please login again", response.json["error"]
|
348
381
|
)
|
349
382
|
|
350
|
-
|
351
|
-
class TestLogInOpenAuthOther(CustomTestCase):
|
352
|
-
def create_app(self):
|
383
|
+
def test_missing_auth_header(self):
|
353
384
|
"""
|
354
|
-
|
355
|
-
|
356
|
-
:returns: A configured Flask application instance
|
357
|
-
:rtype: Flask
|
385
|
+
Tests that missing Authorization header raises proper error
|
358
386
|
"""
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
app.config["OID_CLIENT_ID"] = "SOME_SECRET"
|
363
|
-
app.config["OID_TENANT_ID"] = "SOME_SECRET"
|
364
|
-
app.config["OID_ISSUER"] = "SOME_SECRET"
|
365
|
-
return app
|
366
|
-
|
367
|
-
def setUp(self):
|
368
|
-
log.root.setLevel(current_app.config["LOG_LEVEL"])
|
369
|
-
db.create_all()
|
370
|
-
access_init_command(verbose=False)
|
371
|
-
register_deployed_dags_command_test(verbose=False)
|
372
|
-
|
373
|
-
self.service_data = {
|
374
|
-
"username": "service_user",
|
375
|
-
"email": "service@test.com",
|
376
|
-
"password": "Testpassword1!",
|
377
|
-
}
|
378
|
-
|
379
|
-
service_user = UserModel(data=self.service_data)
|
380
|
-
service_user.save()
|
381
|
-
|
382
|
-
self.service_data.pop("email")
|
383
|
-
self.service_user_id = service_user.id
|
387
|
+
response = self.client.post(
|
388
|
+
LOGIN_URL, data=json.dumps({}), headers={"Content-Type": "application/json"}
|
389
|
+
)
|
384
390
|
|
385
|
-
self.
|
391
|
+
self.assertEqual(400, response.status_code)
|
392
|
+
self.assertEqual(response.json["error"], "Authorization header is missing")
|
386
393
|
|
387
|
-
def
|
394
|
+
def test_invalid_auth_header(self):
|
388
395
|
"""
|
389
|
-
Tests that
|
396
|
+
Tests that malformed Authorization header raises proper error
|
390
397
|
"""
|
391
398
|
response = self.client.post(
|
392
399
|
LOGIN_URL,
|
393
|
-
data=json.dumps({
|
394
|
-
headers={
|
400
|
+
data=json.dumps({}),
|
401
|
+
headers={
|
402
|
+
"Content-Type": "application/json",
|
403
|
+
"Authorization": "Invalid Format",
|
404
|
+
},
|
395
405
|
)
|
396
406
|
|
397
|
-
self.assertEqual(
|
407
|
+
self.assertEqual(400, response.status_code)
|
398
408
|
self.assertEqual(
|
399
|
-
|
409
|
+
"Invalid Authorization header format. Must be 'Bearer <token>'",
|
410
|
+
response.json["error"],
|
400
411
|
)
|
401
412
|
|
402
413
|
|
@@ -410,6 +421,7 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
410
421
|
:rtype: Flask
|
411
422
|
"""
|
412
423
|
app = create_app("testing-oauth")
|
424
|
+
app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] = 1
|
413
425
|
return app
|
414
426
|
|
415
427
|
def setUp(self):
|
@@ -448,7 +460,6 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
448
460
|
"""
|
449
461
|
Tests that a service user can log in with username and password
|
450
462
|
"""
|
451
|
-
|
452
463
|
response = self.client.post(
|
453
464
|
LOGIN_URL,
|
454
465
|
data=json.dumps(self.service_data),
|
@@ -458,7 +469,7 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
458
469
|
self.assertEqual(200, response.status_code)
|
459
470
|
self.assertEqual(self.service_user_id, response.json["id"])
|
460
471
|
|
461
|
-
def
|
472
|
+
def test_no_credentials_error(self):
|
462
473
|
"""
|
463
474
|
Tests that a user can not log in without token or username and password
|
464
475
|
"""
|
@@ -469,6 +480,7 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
469
480
|
)
|
470
481
|
|
471
482
|
self.assertEqual(400, response.status_code)
|
483
|
+
self.assertEqual(response.json["error"], "Authorization header is missing")
|
472
484
|
|
473
485
|
def test_other_user_password(self):
|
474
486
|
"""
|
@@ -483,29 +495,37 @@ class TestLogInOpenAuthService(CustomTestCase):
|
|
483
495
|
self.assertEqual(400, response.status_code)
|
484
496
|
self.assertEqual(response.json["error"], "Invalid request")
|
485
497
|
|
486
|
-
|
498
|
+
@mock.patch("cornflow.shared.authentication.Auth.decode_token")
|
499
|
+
def test_token_login(self, mock_decode):
|
487
500
|
"""
|
488
|
-
Tests that a user can log in with a token
|
501
|
+
Tests that a user can successfully log in with a valid token
|
489
502
|
"""
|
503
|
+
mock_decode.return_value = {"username": "testname"}
|
504
|
+
|
490
505
|
response = self.client.post(
|
491
506
|
LOGIN_URL,
|
492
|
-
data=json.dumps({
|
493
|
-
headers={
|
507
|
+
data=json.dumps({}),
|
508
|
+
headers={
|
509
|
+
"Content-Type": "application/json",
|
510
|
+
"Authorization": "Bearer valid_token",
|
511
|
+
},
|
494
512
|
)
|
495
513
|
|
496
|
-
self.assertEqual(
|
497
|
-
self.assertEqual(
|
498
|
-
response.json["error"], "The OID provider configuration is not valid"
|
499
|
-
)
|
514
|
+
self.assertEqual(200, response.status_code)
|
515
|
+
self.assertEqual(self.test_user_id, response.json["id"])
|
500
516
|
|
501
|
-
def
|
517
|
+
def test_invalid_token_login(self):
|
502
518
|
"""
|
503
|
-
Tests that
|
519
|
+
Tests that login fails with an invalid token
|
504
520
|
"""
|
505
521
|
response = self.client.post(
|
506
522
|
LOGIN_URL,
|
507
|
-
data=json.dumps({
|
508
|
-
headers={
|
523
|
+
data=json.dumps({}),
|
524
|
+
headers={
|
525
|
+
"Content-Type": "application/json",
|
526
|
+
"Authorization": "Bearer invalid_token",
|
527
|
+
},
|
509
528
|
)
|
510
529
|
|
511
530
|
self.assertEqual(400, response.status_code)
|
531
|
+
self.assertEqual(response.json["error"], "Invalid token format or signature")
|