cornflow 2.0.0a10__py3-none-any.whl → 2.0.0a12__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.
Files changed (52) hide show
  1. airflow_config/airflow_local_settings.py +1 -1
  2. cornflow/app.py +8 -3
  3. cornflow/cli/migrations.py +23 -3
  4. cornflow/cli/service.py +18 -18
  5. cornflow/cli/utils.py +16 -1
  6. cornflow/commands/dag.py +1 -1
  7. cornflow/config.py +13 -8
  8. cornflow/endpoints/__init__.py +8 -2
  9. cornflow/endpoints/alarms.py +66 -2
  10. cornflow/endpoints/data_check.py +53 -26
  11. cornflow/endpoints/execution.py +387 -132
  12. cornflow/endpoints/login.py +81 -63
  13. cornflow/endpoints/meta_resource.py +11 -3
  14. cornflow/migrations/versions/999b98e24225.py +34 -0
  15. cornflow/models/base_data_model.py +4 -32
  16. cornflow/models/execution.py +2 -3
  17. cornflow/models/meta_models.py +28 -22
  18. cornflow/models/user.py +7 -10
  19. cornflow/schemas/alarms.py +8 -0
  20. cornflow/schemas/execution.py +1 -1
  21. cornflow/schemas/query.py +2 -1
  22. cornflow/schemas/user.py +5 -20
  23. cornflow/shared/authentication/auth.py +201 -264
  24. cornflow/shared/const.py +3 -14
  25. cornflow/shared/databricks.py +5 -1
  26. cornflow/tests/const.py +1 -0
  27. cornflow/tests/custom_test_case.py +77 -26
  28. cornflow/tests/unit/test_actions.py +2 -2
  29. cornflow/tests/unit/test_alarms.py +55 -1
  30. cornflow/tests/unit/test_apiview.py +108 -3
  31. cornflow/tests/unit/test_cases.py +20 -29
  32. cornflow/tests/unit/test_cli.py +6 -5
  33. cornflow/tests/unit/test_commands.py +3 -3
  34. cornflow/tests/unit/test_dags.py +5 -6
  35. cornflow/tests/unit/test_executions.py +443 -123
  36. cornflow/tests/unit/test_instances.py +14 -2
  37. cornflow/tests/unit/test_instances_file.py +1 -1
  38. cornflow/tests/unit/test_licenses.py +1 -1
  39. cornflow/tests/unit/test_log_in.py +230 -207
  40. cornflow/tests/unit/test_permissions.py +8 -8
  41. cornflow/tests/unit/test_roles.py +48 -10
  42. cornflow/tests/unit/test_schemas.py +1 -1
  43. cornflow/tests/unit/test_tables.py +7 -7
  44. cornflow/tests/unit/test_token.py +19 -5
  45. cornflow/tests/unit/test_users.py +22 -6
  46. cornflow/tests/unit/tools.py +75 -10
  47. {cornflow-2.0.0a10.dist-info → cornflow-2.0.0a12.dist-info}/METADATA +16 -15
  48. {cornflow-2.0.0a10.dist-info → cornflow-2.0.0a12.dist-info}/RECORD +51 -51
  49. {cornflow-2.0.0a10.dist-info → cornflow-2.0.0a12.dist-info}/WHEEL +1 -1
  50. cornflow/endpoints/execution_databricks.py +0 -808
  51. {cornflow-2.0.0a10.dist-info → cornflow-2.0.0a12.dist-info}/entry_points.txt +0 -0
  52. {cornflow-2.0.0a10.dist-info → cornflow-2.0.0a12.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, OID_GOOGLE, OID_AZURE, OID_NONE
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 TestLogInOpenAuthNoConfig(CustomTestCase):
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,294 @@ class TestLogInOpenAuthNoConfig(CustomTestCase):
120
123
  )
121
124
 
122
125
  self.assertEqual(400, response.status_code)
123
- self.assertEqual(response.json["error"], "Invalid request")
124
-
125
- def test_other_user_password(self):
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
- self.assertEqual(400, response.status_code)
136
- self.assertEqual(response.json["error"], "Invalid request")
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
- Creates and configures a Flask application for testing.
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
- app = create_app("testing-oauth")
148
- app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] = 0
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
- def setUp(self):
156
- log.root.setLevel(current_app.config["LOG_LEVEL"])
157
- db.create_all()
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
- self.service_data = {
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
- service_user = UserModel(data=self.service_data)
168
- service_user.save()
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
- self.service_data.pop("email")
171
- self.service_user_id = service_user.id
152
+ mock_jwt.decode.side_effect = decode_side_effect
172
153
 
173
- self.assign_role(self.service_user_id, SERVICE_ROLE)
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({"token": "some_token"}),
182
- headers={"Content-Type": "application/json"},
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(response.json["error"], "Token is not valid")
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 test_service_user_login_second_fail(self, mock_header):
172
+ def test_missing_kid_in_token(self, mock_jwt):
190
173
  """
191
- Tests second exit point on token validation
174
+ Tests token validation failure when the token header is missing the kid
192
175
  """
193
- mock_header.get_unverified_header.return_value = None
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({"token": "some_token"}),
197
- headers={"Content-Type": "application/json"},
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(response.json["error"], "Token is missing the headers")
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 test_service_user_login_third_fail(self, mock_header):
203
+ def test_public_keys_fetch_fail(self, mock_jwt, mock_get):
205
204
  """
206
- Tests third exit point on token validation
205
+ Tests failure when trying to fetch public keys from the OIDC provider
207
206
  """
208
- mock_header.get_unverified_header.return_value = {"kid": "some_value"}
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({"token": "some_token"}),
212
- headers={"Content-Type": "application/json"},
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.assertIn(
217
- "Error getting issuer discovery meta from", response.json["error"]
242
+ self.assertEqual(
243
+ response.json["error"],
244
+ "Failed to fetch public keys from authentication provider",
218
245
  )
219
246
 
220
- @mock.patch("cornflow.endpoints.login.Auth.validate_oid_token")
221
- def test_service_user_login_no_fail(self, mock_auth):
222
- mock_auth.return_value = {"preferred_username": "service_user"}
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 = {"sub": "service_user"}
223
253
  response = self.client.post(
224
254
  LOGIN_URL,
225
- data=json.dumps({"token": "some_token"}),
226
- headers={"Content-Type": "application/json"},
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.endpoints.login.Auth.validate_oid_token")
235
- def test_new_user_login_no_fail(self, mock_auth):
236
- mock_auth.return_value = {"preferred_username": "test_user"}
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 = {"sub": "test_user"}
237
271
  response = self.client.post(
238
272
  LOGIN_URL,
239
- data=json.dumps({"token": "some_token"}),
240
- headers={"Content-Type": "application/json"},
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
- class TestLogInOpenAuthGoogle(CustomTestCase):
248
- def create_app(self):
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
- Creates and configures a Flask application for testing.
251
-
252
- :returns: A configured Flask application instance
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
- app = create_app("testing-oauth")
256
- app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] = 0
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
- def setUp(self):
264
- log.root.setLevel(current_app.config["LOG_LEVEL"])
265
- db.create_all()
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
- self.service_data = {
270
- "username": "service_user",
271
- "email": "service@test.com",
272
- "password": "Testpassword1!",
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_user@test.com"}
304
+ )
274
305
 
275
- service_user = UserModel(data=self.service_data)
276
- service_user.save()
306
+ # Mock verify_token to always return a valid token payload
307
+ mock_verify_token.return_value = {
308
+ "sub": "test_user",
309
+ "email": "test_user@test.com",
310
+ }
277
311
 
278
- self.service_data.pop("email")
279
- self.service_user_id = service_user.id
312
+ # Make first request
313
+ response = self.client.post(
314
+ LOGIN_URL,
315
+ data=json.dumps({}),
316
+ headers={
317
+ "Content-Type": "application/json",
318
+ "Authorization": "Bearer some_token",
319
+ },
320
+ )
280
321
 
281
- self.assign_role(self.service_user_id, SERVICE_ROLE)
322
+ self.assertEqual(200, response.status_code)
282
323
 
283
- def test_service_user_login(self):
284
- """
285
- Tests that a service user can not log in with username and password
286
- """
324
+ # Make second request
287
325
  response = self.client.post(
288
326
  LOGIN_URL,
289
- data=json.dumps({"token": "some_token"}),
290
- headers={"Content-Type": "application/json"},
327
+ data=json.dumps({}),
328
+ headers={
329
+ "Content-Type": "application/json",
330
+ "Authorization": "Bearer some_token",
331
+ },
291
332
  )
292
333
 
293
- self.assertEqual(501, response.status_code)
294
- self.assertEqual(
295
- response.json["error"], "The selected OID provider is not implemented"
296
- )
334
+ self.assertEqual(200, response.status_code)
297
335
 
336
+ # Verify token was verified twice
337
+ self.assertEqual(2, mock_verify_token.call_count)
298
338
 
299
- class TestLogInOpenAuthNone(CustomTestCase):
300
- def create_app(self):
301
- """
302
- Creates and configures a Flask application for testing.
339
+ # Now test with a different token
340
+ response = self.client.post(
341
+ LOGIN_URL,
342
+ data=json.dumps({}),
343
+ headers={
344
+ "Content-Type": "application/json",
345
+ "Authorization": "Bearer some_different_token",
346
+ },
347
+ )
303
348
 
304
- :returns: A configured Flask application instance
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
349
+ self.assertEqual(200, response.status_code)
314
350
 
315
- def setUp(self):
316
- log.root.setLevel(current_app.config["LOG_LEVEL"])
317
- db.create_all()
318
- access_init_command(verbose=False)
319
- register_deployed_dags_command_test(verbose=False)
351
+ # Verify token was verified a third time
352
+ self.assertEqual(3, mock_verify_token.call_count)
320
353
 
321
- self.service_data = {
322
- "username": "service_user",
323
- "email": "service@test.com",
324
- "password": "Testpassword1!",
354
+ def test_old_token(self):
355
+ """
356
+ Tests using an expired token.
357
+ """
358
+ # Generate an expired token
359
+ payload = {
360
+ # Token expired 1 hour ago
361
+ "exp": datetime.utcnow() - timedelta(hours=1),
362
+ # Token created 2 hours ago
363
+ "iat": datetime.utcnow() - timedelta(hours=2),
364
+ "sub": "testname",
365
+ "iss": INTERNAL_TOKEN_ISSUER,
325
366
  }
326
367
 
327
- service_user = UserModel(data=self.service_data)
328
- service_user.save()
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)
368
+ expired_token = jwt.encode(
369
+ payload, current_app.config["SECRET_TOKEN_KEY"], algorithm="HS256"
370
+ )
334
371
 
335
- def test_service_user_login(self):
336
- """
337
- Tests that a service user can not log in with username and password
338
- """
339
372
  response = self.client.post(
340
373
  LOGIN_URL,
341
- data=json.dumps({"token": "some_token"}),
342
- headers={"Content-Type": "application/json"},
374
+ data=json.dumps({}),
375
+ headers={
376
+ "Content-Type": "application/json",
377
+ "Authorization": f"Bearer {expired_token}",
378
+ },
343
379
  )
344
380
 
345
- self.assertEqual(501, response.status_code)
381
+ self.assertEqual(400, response.status_code)
346
382
  self.assertEqual(
347
- response.json["error"], "The OID provider configuration is not valid"
383
+ "The token has expired, please login again", response.json["error"]
348
384
  )
349
385
 
350
-
351
- class TestLogInOpenAuthOther(CustomTestCase):
352
- def create_app(self):
386
+ def test_missing_auth_header(self):
353
387
  """
354
- Creates and configures a Flask application for testing.
355
-
356
- :returns: A configured Flask application instance
357
- :rtype: Flask
388
+ Tests that missing Authorization header raises proper error
358
389
  """
359
- app = create_app("testing-oauth")
360
- app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] = 0
361
- app.config["OID_PROVIDER"] = 3
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
390
+ response = self.client.post(
391
+ LOGIN_URL, data=json.dumps({}), headers={"Content-Type": "application/json"}
392
+ )
384
393
 
385
- self.assign_role(self.service_user_id, SERVICE_ROLE)
394
+ self.assertEqual(400, response.status_code)
395
+ self.assertEqual(response.json["error"], "Authorization header is missing")
386
396
 
387
- def test_service_user_login(self):
397
+ def test_invalid_auth_header(self):
388
398
  """
389
- Tests that a service user can not log in with username and password
399
+ Tests that malformed Authorization header raises proper error
390
400
  """
391
401
  response = self.client.post(
392
402
  LOGIN_URL,
393
- data=json.dumps({"token": "some_token"}),
394
- headers={"Content-Type": "application/json"},
403
+ data=json.dumps({}),
404
+ headers={
405
+ "Content-Type": "application/json",
406
+ "Authorization": "Invalid Format",
407
+ },
395
408
  )
396
409
 
397
- self.assertEqual(501, response.status_code)
410
+ self.assertEqual(400, response.status_code)
398
411
  self.assertEqual(
399
- response.json["error"], "The OID provider configuration is not valid"
412
+ "Invalid Authorization header format. Must be 'Bearer <token>'",
413
+ response.json["error"],
400
414
  )
401
415
 
402
416
 
@@ -410,6 +424,7 @@ class TestLogInOpenAuthService(CustomTestCase):
410
424
  :rtype: Flask
411
425
  """
412
426
  app = create_app("testing-oauth")
427
+ app.config["SERVICE_USER_ALLOW_PASSWORD_LOGIN"] = 1
413
428
  return app
414
429
 
415
430
  def setUp(self):
@@ -448,7 +463,6 @@ class TestLogInOpenAuthService(CustomTestCase):
448
463
  """
449
464
  Tests that a service user can log in with username and password
450
465
  """
451
-
452
466
  response = self.client.post(
453
467
  LOGIN_URL,
454
468
  data=json.dumps(self.service_data),
@@ -458,7 +472,7 @@ class TestLogInOpenAuthService(CustomTestCase):
458
472
  self.assertEqual(200, response.status_code)
459
473
  self.assertEqual(self.service_user_id, response.json["id"])
460
474
 
461
- def test_validation_error(self):
475
+ def test_no_credentials_error(self):
462
476
  """
463
477
  Tests that a user can not log in without token or username and password
464
478
  """
@@ -469,6 +483,7 @@ class TestLogInOpenAuthService(CustomTestCase):
469
483
  )
470
484
 
471
485
  self.assertEqual(400, response.status_code)
486
+ self.assertEqual(response.json["error"], "Authorization header is missing")
472
487
 
473
488
  def test_other_user_password(self):
474
489
  """
@@ -483,29 +498,37 @@ class TestLogInOpenAuthService(CustomTestCase):
483
498
  self.assertEqual(400, response.status_code)
484
499
  self.assertEqual(response.json["error"], "Invalid request")
485
500
 
486
- def test_token_login(self):
501
+ @mock.patch("cornflow.shared.authentication.Auth.decode_token")
502
+ def test_token_login(self, mock_decode):
487
503
  """
488
- Tests that a user can log in with a token
504
+ Tests that a user can successfully log in with a valid token
489
505
  """
506
+ mock_decode.return_value = {"sub": "testname"}
507
+
490
508
  response = self.client.post(
491
509
  LOGIN_URL,
492
- data=json.dumps({"token": "test"}),
493
- headers={"Content-Type": "application/json"},
510
+ data=json.dumps({}),
511
+ headers={
512
+ "Content-Type": "application/json",
513
+ "Authorization": "Bearer valid_token",
514
+ },
494
515
  )
495
516
 
496
- self.assertEqual(501, response.status_code)
497
- self.assertEqual(
498
- response.json["error"], "The OID provider configuration is not valid"
499
- )
517
+ self.assertEqual(200, response.status_code)
518
+ self.assertEqual(self.test_user_id, response.json["id"])
500
519
 
501
- def test_log_in_with_all_fields(self):
520
+ def test_invalid_token_login(self):
502
521
  """
503
- Tests that a user can not log in with username and password and a token
522
+ Tests that login fails with an invalid token
504
523
  """
505
524
  response = self.client.post(
506
525
  LOGIN_URL,
507
- data=json.dumps({**self.service_data, "token": "test"}),
508
- headers={"Content-Type": "application/json"},
526
+ data=json.dumps({}),
527
+ headers={
528
+ "Content-Type": "application/json",
529
+ "Authorization": "Bearer invalid_token",
530
+ },
509
531
  )
510
532
 
511
533
  self.assertEqual(400, response.status_code)
534
+ self.assertEqual(response.json["error"], "Invalid token format or signature")