cornflow 1.2.3a4__py3-none-any.whl → 1.2.4__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 +24 -8
- cornflow/cli/service.py +2 -1
- cornflow/commands/auxiliar.py +12 -3
- cornflow/commands/permissions.py +32 -11
- cornflow/commands/roles.py +1 -1
- cornflow/commands/views.py +3 -0
- cornflow/config.py +4 -4
- cornflow/endpoints/__init__.py +27 -0
- cornflow/endpoints/permission.py +1 -2
- cornflow/endpoints/signup.py +17 -11
- cornflow/shared/authentication/decorators.py +33 -2
- cornflow/shared/const.py +15 -1
- cornflow/tests/unit/test_apiview.py +7 -1
- cornflow/tests/unit/test_cli.py +6 -3
- cornflow/tests/unit/test_commands.py +2 -1
- cornflow/tests/unit/test_external_role_creation.py +246 -0
- cornflow/tests/unit/test_get_resources.py +103 -0
- cornflow/tests/unit/test_sign_up.py +181 -6
- {cornflow-1.2.3a4.dist-info → cornflow-1.2.4.dist-info}/METADATA +3 -3
- {cornflow-1.2.3a4.dist-info → cornflow-1.2.4.dist-info}/RECORD +23 -22
- {cornflow-1.2.3a4.dist-info → cornflow-1.2.4.dist-info}/WHEEL +0 -0
- {cornflow-1.2.3a4.dist-info → cornflow-1.2.4.dist-info}/entry_points.txt +0 -0
- {cornflow-1.2.3a4.dist-info → cornflow-1.2.4.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,7 @@ from cornflow.shared.const import (
|
|
13
13
|
PATCH_ACTION,
|
14
14
|
DELETE_ACTION,
|
15
15
|
GET_ACTION,
|
16
|
+
PUT_ACTION,
|
16
17
|
)
|
17
18
|
|
18
19
|
|
@@ -106,6 +107,13 @@ class ExternalRoleCreationTestCase(CustomTestCase):
|
|
106
107
|
(VIEWER_ROLE, POST_ACTION, "scheduling_optimizer"),
|
107
108
|
(PLANNER_ROLE, DELETE_ACTION, "quality_control"),
|
108
109
|
]
|
110
|
+
# Define permissions for custom roles used in endpoints
|
111
|
+
mock_const.CUSTOM_ROLES_ACTIONS = {
|
112
|
+
# Default permission for role 888
|
113
|
+
888: [GET_ACTION],
|
114
|
+
# Default permission for role 777
|
115
|
+
777: [GET_ACTION],
|
116
|
+
}
|
109
117
|
mock_shared.const = mock_const
|
110
118
|
mock_external_app.shared = mock_shared
|
111
119
|
|
@@ -206,6 +214,17 @@ class ExternalRoleCreationTestCase(CustomTestCase):
|
|
206
214
|
(10000, POST_ACTION, "production_planning"),
|
207
215
|
(999, PATCH_ACTION, "quality_control"),
|
208
216
|
]
|
217
|
+
# Define permissions for custom roles used in endpoints (888, 777) and test roles (10000, 999)
|
218
|
+
mock_const.CUSTOM_ROLES_ACTIONS = {
|
219
|
+
# Used in endpoints
|
220
|
+
888: [GET_ACTION],
|
221
|
+
# Used in endpoints
|
222
|
+
777: [GET_ACTION],
|
223
|
+
# Used in test
|
224
|
+
10000: [GET_ACTION],
|
225
|
+
# Used in test
|
226
|
+
999: [GET_ACTION],
|
227
|
+
}
|
209
228
|
mock_shared.const = mock_const
|
210
229
|
mock_external_app.shared = mock_shared
|
211
230
|
|
@@ -224,6 +243,7 @@ class ExternalRoleCreationTestCase(CustomTestCase):
|
|
224
243
|
mock_const.EXTRA_PERMISSION_ASSIGNATION = [
|
225
244
|
(10000, POST_ACTION, "production_planning"),
|
226
245
|
]
|
246
|
+
# Keep the same CUSTOM_ROLES_ACTIONS (role definitions don't change)
|
227
247
|
|
228
248
|
# Re-run permissions registration
|
229
249
|
access_init_command(verbose=True)
|
@@ -266,6 +286,13 @@ class ExternalRoleCreationTestCase(CustomTestCase):
|
|
266
286
|
mock_const = MagicMock()
|
267
287
|
# Don't set EXTRA_PERMISSION_ASSIGNATION to trigger AttributeError
|
268
288
|
del mock_const.EXTRA_PERMISSION_ASSIGNATION
|
289
|
+
# Define permissions for custom roles used in endpoints
|
290
|
+
mock_const.CUSTOM_ROLES_ACTIONS = {
|
291
|
+
# Used in endpoints
|
292
|
+
888: [GET_ACTION],
|
293
|
+
# Used in endpoints
|
294
|
+
777: [GET_ACTION],
|
295
|
+
}
|
269
296
|
mock_shared.const = mock_const
|
270
297
|
mock_external_app.shared = mock_shared
|
271
298
|
|
@@ -304,6 +331,13 @@ class ExternalRoleCreationTestCase(CustomTestCase):
|
|
304
331
|
(VIEWER_ROLE, POST_ACTION, "production_planning"), # Extend standard role
|
305
332
|
(PLANNER_ROLE, DELETE_ACTION, "quality_control"), # Extend standard role
|
306
333
|
]
|
334
|
+
# Define permissions for custom roles used in endpoints
|
335
|
+
mock_const.CUSTOM_ROLES_ACTIONS = {
|
336
|
+
# Used in endpoints
|
337
|
+
888: [GET_ACTION],
|
338
|
+
# Used in endpoints
|
339
|
+
777: [GET_ACTION],
|
340
|
+
}
|
307
341
|
mock_shared.const = mock_const
|
308
342
|
mock_external_app.shared = mock_shared
|
309
343
|
|
@@ -385,6 +419,13 @@ class ExternalRoleCreationTestCase(CustomTestCase):
|
|
385
419
|
mock_shared_initial = MagicMock()
|
386
420
|
mock_const_initial = MagicMock()
|
387
421
|
mock_const_initial.EXTRA_PERMISSION_ASSIGNATION = []
|
422
|
+
# Define permissions for custom roles used in test endpoints
|
423
|
+
mock_const_initial.CUSTOM_ROLES_ACTIONS = {
|
424
|
+
# Used in test endpoints
|
425
|
+
888: [GET_ACTION],
|
426
|
+
# Used in test endpoints
|
427
|
+
777: [GET_ACTION],
|
428
|
+
}
|
388
429
|
mock_shared_initial.const = mock_const_initial
|
389
430
|
mock_external_app_initial.shared = mock_shared_initial
|
390
431
|
|
@@ -459,6 +500,13 @@ class ExternalRoleCreationTestCase(CustomTestCase):
|
|
459
500
|
mock_shared_updated = MagicMock()
|
460
501
|
mock_const_updated = MagicMock()
|
461
502
|
mock_const_updated.EXTRA_PERMISSION_ASSIGNATION = [] # Empty list
|
503
|
+
# Define permissions for custom roles used in test endpoints
|
504
|
+
mock_const_updated.CUSTOM_ROLES_ACTIONS = {
|
505
|
+
# Used in test endpoints
|
506
|
+
888: [GET_ACTION],
|
507
|
+
# Used in test endpoints
|
508
|
+
777: [GET_ACTION],
|
509
|
+
}
|
462
510
|
mock_shared_updated.const = mock_const_updated
|
463
511
|
mock_external_app_updated.shared = mock_shared_updated
|
464
512
|
|
@@ -534,6 +582,204 @@ class ExternalRoleCreationTestCase(CustomTestCase):
|
|
534
582
|
f"Permissions for remaining views should still exist. Found views: {set(all_view_names)}",
|
535
583
|
)
|
536
584
|
|
585
|
+
@patch("cornflow.commands.auxiliar.import_module")
|
586
|
+
@patch("cornflow.commands.views.import_module")
|
587
|
+
@patch.dict(
|
588
|
+
os.environ, {"EXTERNAL_APP": "1", "EXTERNAL_APP_MODULE": "external_test_app"}
|
589
|
+
)
|
590
|
+
def test_custom_roles_actions_success(
|
591
|
+
self, mock_import_views, mock_import_auxiliar
|
592
|
+
):
|
593
|
+
"""
|
594
|
+
Test that custom roles get their defined actions from CUSTOM_ROLES_ACTIONS
|
595
|
+
"""
|
596
|
+
# Mock external app configuration
|
597
|
+
mock_external_app = MagicMock()
|
598
|
+
|
599
|
+
# Mock the shared.const module with CUSTOM_ROLES_ACTIONS
|
600
|
+
mock_shared = MagicMock()
|
601
|
+
mock_const = MagicMock()
|
602
|
+
mock_const.EXTRA_PERMISSION_ASSIGNATION = []
|
603
|
+
# Define custom roles actions
|
604
|
+
mock_const.CUSTOM_ROLES_ACTIONS = {
|
605
|
+
# Custom role with multiple actions
|
606
|
+
888: [
|
607
|
+
GET_ACTION,
|
608
|
+
POST_ACTION,
|
609
|
+
PUT_ACTION,
|
610
|
+
],
|
611
|
+
# Another custom role with different actions
|
612
|
+
777: [
|
613
|
+
GET_ACTION,
|
614
|
+
PATCH_ACTION,
|
615
|
+
],
|
616
|
+
}
|
617
|
+
mock_shared.const = mock_const
|
618
|
+
mock_external_app.shared = mock_shared
|
619
|
+
|
620
|
+
# Mock the endpoints.resources with fake external app endpoints
|
621
|
+
mock_endpoints = MagicMock()
|
622
|
+
mock_endpoints.resources = self._create_mock_external_app_resources()
|
623
|
+
mock_external_app.endpoints = mock_endpoints
|
624
|
+
|
625
|
+
mock_import_views.return_value = mock_external_app
|
626
|
+
mock_import_auxiliar.return_value = mock_external_app
|
627
|
+
|
628
|
+
# Mock the database session for testing
|
629
|
+
with patch.object(db.session, "commit"):
|
630
|
+
with patch.object(db.session, "rollback"):
|
631
|
+
# Run the complete access initialization
|
632
|
+
access_init_command(verbose=True)
|
633
|
+
|
634
|
+
# Verify that custom permissions were created with the correct actions
|
635
|
+
from cornflow.models import PermissionViewRoleModel
|
636
|
+
|
637
|
+
# Get all permissions for role 888
|
638
|
+
permissions_888 = PermissionViewRoleModel.query.filter_by(
|
639
|
+
role_id=888
|
640
|
+
).all()
|
641
|
+
actions_888 = {perm.action_id for perm in permissions_888}
|
642
|
+
|
643
|
+
# Get all permissions for role 777
|
644
|
+
permissions_777 = PermissionViewRoleModel.query.filter_by(
|
645
|
+
role_id=777
|
646
|
+
).all()
|
647
|
+
actions_777 = {perm.action_id for perm in permissions_777}
|
648
|
+
|
649
|
+
# Verify role 888 has GET, POST, PUT actions
|
650
|
+
expected_actions_888 = {GET_ACTION, POST_ACTION, PUT_ACTION}
|
651
|
+
self.assertTrue(
|
652
|
+
expected_actions_888.issubset(actions_888),
|
653
|
+
f"Role 888 should have actions {expected_actions_888}, but got {actions_888}",
|
654
|
+
)
|
655
|
+
|
656
|
+
# Verify role 777 has GET, PATCH actions
|
657
|
+
expected_actions_777 = {GET_ACTION, PATCH_ACTION}
|
658
|
+
self.assertTrue(
|
659
|
+
expected_actions_777.issubset(actions_777),
|
660
|
+
f"Role 777 should have actions {expected_actions_777}, but got {actions_777}",
|
661
|
+
)
|
662
|
+
|
663
|
+
# Verify that role 888 does NOT have actions that were not defined
|
664
|
+
# We check that no DELETE or PATCH actions exist for role 888
|
665
|
+
forbidden_actions_888 = {DELETE_ACTION, PATCH_ACTION}
|
666
|
+
actual_forbidden_888 = actions_888.intersection(forbidden_actions_888)
|
667
|
+
self.assertEqual(
|
668
|
+
len(actual_forbidden_888),
|
669
|
+
0,
|
670
|
+
f"Role 888 should not have actions {forbidden_actions_888}, but found {actual_forbidden_888}",
|
671
|
+
)
|
672
|
+
|
673
|
+
# Verify that role 777 does NOT have actions that were not defined
|
674
|
+
# We check that no POST, PUT, DELETE actions exist for role 777
|
675
|
+
forbidden_actions_777 = {POST_ACTION, PUT_ACTION, DELETE_ACTION}
|
676
|
+
actual_forbidden_777 = actions_777.intersection(forbidden_actions_777)
|
677
|
+
self.assertEqual(
|
678
|
+
len(actual_forbidden_777),
|
679
|
+
0,
|
680
|
+
f"Role 777 should not have actions {forbidden_actions_777}, but found {actual_forbidden_777}",
|
681
|
+
)
|
682
|
+
|
683
|
+
@patch("cornflow.commands.auxiliar.import_module")
|
684
|
+
@patch("cornflow.commands.views.import_module")
|
685
|
+
@patch.dict(
|
686
|
+
os.environ, {"EXTERNAL_APP": "1", "EXTERNAL_APP_MODULE": "external_test_app"}
|
687
|
+
)
|
688
|
+
def test_custom_roles_actions_error_on_undefined_role(
|
689
|
+
self, mock_import_views, mock_import_auxiliar
|
690
|
+
):
|
691
|
+
"""
|
692
|
+
Test that an error is raised when a custom role is used but not defined in CUSTOM_ROLES_ACTIONS
|
693
|
+
"""
|
694
|
+
# Mock external app configuration
|
695
|
+
mock_external_app = MagicMock()
|
696
|
+
|
697
|
+
# Mock the shared.const module with CUSTOM_ROLES_ACTIONS that doesn't include role 888
|
698
|
+
mock_shared = MagicMock()
|
699
|
+
mock_const = MagicMock()
|
700
|
+
mock_const.EXTRA_PERMISSION_ASSIGNATION = []
|
701
|
+
# Define custom roles permissions but MISSING role 888 which is used in endpoints
|
702
|
+
mock_const.CUSTOM_ROLES_ACTIONS = {
|
703
|
+
# Only define role 777, but role 888 is used in endpoints
|
704
|
+
777: [
|
705
|
+
GET_ACTION,
|
706
|
+
PATCH_ACTION,
|
707
|
+
],
|
708
|
+
}
|
709
|
+
mock_shared.const = mock_const
|
710
|
+
mock_external_app.shared = mock_shared
|
711
|
+
|
712
|
+
# Mock the endpoints.resources with fake external app endpoints that use role 888
|
713
|
+
mock_endpoints = MagicMock()
|
714
|
+
mock_endpoints.resources = (
|
715
|
+
self._create_mock_external_app_resources()
|
716
|
+
) # This includes role 888
|
717
|
+
mock_external_app.endpoints = mock_endpoints
|
718
|
+
|
719
|
+
mock_import_views.return_value = mock_external_app
|
720
|
+
mock_import_auxiliar.return_value = mock_external_app
|
721
|
+
|
722
|
+
# Verify that a ValueError is raised for undefined role 888
|
723
|
+
with self.assertRaises(ValueError) as context:
|
724
|
+
access_init_command(verbose=True)
|
725
|
+
|
726
|
+
# Verify the error message contains the undefined role
|
727
|
+
error_message = str(context.exception)
|
728
|
+
self.assertIn("888", error_message)
|
729
|
+
self.assertIn("CUSTOM_ROLES_ACTIONS", error_message)
|
730
|
+
self.assertIn("not defined", error_message)
|
731
|
+
|
732
|
+
@patch("cornflow.commands.auxiliar.import_module")
|
733
|
+
@patch("cornflow.commands.views.import_module")
|
734
|
+
@patch.dict(
|
735
|
+
os.environ, {"EXTERNAL_APP": "1", "EXTERNAL_APP_MODULE": "external_test_app"}
|
736
|
+
)
|
737
|
+
def test_custom_roles_actions_fallback_when_not_defined(
|
738
|
+
self, mock_import_views, mock_import_auxiliar
|
739
|
+
):
|
740
|
+
"""
|
741
|
+
Test that the system falls back gracefully when CUSTOM_ROLES_ACTIONS is not defined
|
742
|
+
but no custom roles are used
|
743
|
+
"""
|
744
|
+
# Mock external app configuration
|
745
|
+
mock_external_app = MagicMock()
|
746
|
+
|
747
|
+
# Mock the shared.const module WITHOUT CUSTOM_ROLES_ACTIONS
|
748
|
+
mock_shared = MagicMock()
|
749
|
+
mock_const = MagicMock()
|
750
|
+
mock_const.EXTRA_PERMISSION_ASSIGNATION = []
|
751
|
+
# Don't set CUSTOM_ROLES_ACTIONS to trigger AttributeError
|
752
|
+
# This simulates an external app that doesn't define the new constant
|
753
|
+
mock_shared.const = mock_const
|
754
|
+
mock_external_app.shared = mock_shared
|
755
|
+
|
756
|
+
# Mock endpoints that only use standard roles (no custom roles)
|
757
|
+
mock_production_endpoint = MagicMock()
|
758
|
+
# Only standard role
|
759
|
+
mock_production_endpoint.ROLES_WITH_ACCESS = [PLANNER_ROLE]
|
760
|
+
mock_production_endpoint.DESCRIPTION = "Production planning endpoint"
|
761
|
+
|
762
|
+
mock_resources = [
|
763
|
+
{
|
764
|
+
"endpoint": "production_planning",
|
765
|
+
"urls": "/production-planning/",
|
766
|
+
"resource": mock_production_endpoint,
|
767
|
+
}
|
768
|
+
]
|
769
|
+
|
770
|
+
mock_endpoints = MagicMock()
|
771
|
+
mock_endpoints.resources = mock_resources
|
772
|
+
mock_external_app.endpoints = mock_endpoints
|
773
|
+
|
774
|
+
mock_import_views.return_value = mock_external_app
|
775
|
+
mock_import_auxiliar.return_value = mock_external_app
|
776
|
+
|
777
|
+
# Should not raise any exceptions since no custom roles are used
|
778
|
+
try:
|
779
|
+
access_init_command(verbose=True)
|
780
|
+
except Exception as e:
|
781
|
+
self.fail(f"access_init_command raised an exception when it shouldn't: {e}")
|
782
|
+
|
537
783
|
|
538
784
|
if __name__ == "__main__":
|
539
785
|
unittest.main()
|
@@ -0,0 +1,103 @@
|
|
1
|
+
"""
|
2
|
+
Unit tests for the get_resources function
|
3
|
+
"""
|
4
|
+
|
5
|
+
import unittest
|
6
|
+
from unittest.mock import patch, MagicMock
|
7
|
+
from flask import Flask
|
8
|
+
from cornflow.endpoints import get_resources, resources
|
9
|
+
from cornflow.shared.const import CONDITIONAL_ENDPOINTS
|
10
|
+
|
11
|
+
|
12
|
+
class TestGetResources(unittest.TestCase):
|
13
|
+
"""Test the get_resources function"""
|
14
|
+
|
15
|
+
def setUp(self):
|
16
|
+
self.app = Flask(__name__)
|
17
|
+
self.mock_signup_view = MagicMock()
|
18
|
+
self.mock_signup_view.view_class = MagicMock()
|
19
|
+
self.mock_login_view = MagicMock()
|
20
|
+
self.mock_login_view.view_class = MagicMock()
|
21
|
+
|
22
|
+
def test_returns_base_resources_when_no_conditional_endpoints(self):
|
23
|
+
"""Test that get_resources returns base resources when no conditional endpoints are registered"""
|
24
|
+
with self.app.app_context():
|
25
|
+
with patch("flask.current_app.view_functions", {}):
|
26
|
+
result = get_resources()
|
27
|
+
self.assertEqual(result, resources)
|
28
|
+
|
29
|
+
def test_adds_conditional_endpoint_when_registered(self):
|
30
|
+
"""Test that get_resources adds conditional endpoints when they are registered"""
|
31
|
+
with self.app.app_context():
|
32
|
+
mock_view_functions = {"signup": self.mock_signup_view}
|
33
|
+
|
34
|
+
with patch("flask.current_app.view_functions", mock_view_functions):
|
35
|
+
result = get_resources()
|
36
|
+
|
37
|
+
# Should have one more resource than base
|
38
|
+
self.assertEqual(len(result), len(resources) + 1)
|
39
|
+
|
40
|
+
# Check that signup endpoint was added correctly
|
41
|
+
signup_resource = next(
|
42
|
+
(r for r in result if r["endpoint"] == "signup"), None
|
43
|
+
)
|
44
|
+
self.assertIsNotNone(signup_resource)
|
45
|
+
self.assertEqual(
|
46
|
+
signup_resource["urls"], CONDITIONAL_ENDPOINTS["signup"]
|
47
|
+
)
|
48
|
+
self.assertEqual(
|
49
|
+
signup_resource["resource"], self.mock_signup_view.view_class
|
50
|
+
)
|
51
|
+
|
52
|
+
def test_adds_both_signup_and_login_when_registered(self):
|
53
|
+
"""Test that get_resources adds both signup and login when both are registered"""
|
54
|
+
with self.app.app_context():
|
55
|
+
mock_view_functions = {
|
56
|
+
"signup": self.mock_signup_view,
|
57
|
+
"login": self.mock_login_view,
|
58
|
+
}
|
59
|
+
|
60
|
+
with patch("flask.current_app.view_functions", mock_view_functions):
|
61
|
+
result = get_resources()
|
62
|
+
|
63
|
+
# Should have two more resources than base
|
64
|
+
self.assertEqual(len(result), len(resources) + 2)
|
65
|
+
|
66
|
+
# Check that both endpoints were added correctly
|
67
|
+
signup_resource = next(
|
68
|
+
(r for r in result if r["endpoint"] == "signup"), None
|
69
|
+
)
|
70
|
+
login_resource = next(
|
71
|
+
(r for r in result if r["endpoint"] == "login"), None
|
72
|
+
)
|
73
|
+
|
74
|
+
self.assertIsNotNone(signup_resource)
|
75
|
+
self.assertIsNotNone(login_resource)
|
76
|
+
self.assertEqual(
|
77
|
+
signup_resource["urls"], CONDITIONAL_ENDPOINTS["signup"]
|
78
|
+
)
|
79
|
+
self.assertEqual(login_resource["urls"], CONDITIONAL_ENDPOINTS["login"])
|
80
|
+
|
81
|
+
def test_does_not_add_duplicate_endpoints(self):
|
82
|
+
"""Test that get_resources does not add endpoints that already exist in base resources"""
|
83
|
+
with self.app.app_context():
|
84
|
+
mock_view_functions = {
|
85
|
+
"instance": MagicMock()
|
86
|
+
} # 'instance' already exists in base resources
|
87
|
+
|
88
|
+
with patch("flask.current_app.view_functions", mock_view_functions):
|
89
|
+
result = get_resources()
|
90
|
+
self.assertEqual(len(result), len(resources))
|
91
|
+
|
92
|
+
def test_ignores_non_conditional_endpoints(self):
|
93
|
+
"""Test that get_resources ignores endpoints that are not in CONDITIONAL_ENDPOINTS"""
|
94
|
+
with self.app.app_context():
|
95
|
+
mock_view_functions = {"non_conditional_endpoint": MagicMock()}
|
96
|
+
|
97
|
+
with patch("flask.current_app.view_functions", mock_view_functions):
|
98
|
+
result = get_resources()
|
99
|
+
self.assertEqual(len(result), len(resources))
|
100
|
+
|
101
|
+
|
102
|
+
if __name__ == "__main__":
|
103
|
+
unittest.main()
|
@@ -1,18 +1,25 @@
|
|
1
1
|
"""
|
2
2
|
Unit test for the sign-up endpoint
|
3
3
|
"""
|
4
|
-
from cornflow.commands import access_init_command
|
5
|
-
from cornflow.commands.dag import register_deployed_dags_command_test
|
6
4
|
|
7
5
|
# Import from libraries
|
8
|
-
|
6
|
+
|
7
|
+
# Import from internal modules
|
8
|
+
|
9
9
|
import json
|
10
|
+
from unittest.mock import patch
|
11
|
+
|
12
|
+
# Import from libraries
|
13
|
+
from flask_testing import TestCase
|
10
14
|
|
11
15
|
# Import from internal modules
|
12
16
|
from cornflow.app import create_app
|
13
|
-
from cornflow.
|
14
|
-
from cornflow.
|
17
|
+
from cornflow.commands import access_init_command
|
18
|
+
from cornflow.commands.dag import register_deployed_dags_command_test
|
19
|
+
from cornflow.models import UserModel, UserRoleModel, ViewModel, PermissionViewRoleModel
|
15
20
|
from cornflow.shared import db
|
21
|
+
from cornflow.shared.authentication import Auth
|
22
|
+
from cornflow.shared.const import PLANNER_ROLE, ADMIN_ROLE, POST_ACTION, NO_SIGNUP, SIGNUP_WITH_AUTH
|
16
23
|
from cornflow.tests.const import SIGNUP_URL
|
17
24
|
|
18
25
|
|
@@ -95,7 +102,7 @@ class TestSignUp(TestCase):
|
|
95
102
|
class TestSignUpDeactivated(TestCase):
|
96
103
|
def create_app(self):
|
97
104
|
app = create_app("testing")
|
98
|
-
app.config["SIGNUP_ACTIVATED"] =
|
105
|
+
app.config["SIGNUP_ACTIVATED"] = NO_SIGNUP
|
99
106
|
return app
|
100
107
|
|
101
108
|
def setUp(self):
|
@@ -120,3 +127,171 @@ class TestSignUpDeactivated(TestCase):
|
|
120
127
|
)
|
121
128
|
|
122
129
|
self.assertEqual(response.status_code, 400)
|
130
|
+
|
131
|
+
|
132
|
+
class TestSignUpAuthenticated(TestCase):
|
133
|
+
"""Test the authenticated signup endpoint (SIGNUP_ACTIVATED=SIGNUP_WITH_AUTH)"""
|
134
|
+
|
135
|
+
def create_app(self):
|
136
|
+
with patch("cornflow.config.Testing.SIGNUP_ACTIVATED", SIGNUP_WITH_AUTH):
|
137
|
+
app = create_app("testing")
|
138
|
+
return app
|
139
|
+
|
140
|
+
def setUp(self):
|
141
|
+
db.create_all()
|
142
|
+
access_init_command(verbose=False)
|
143
|
+
register_deployed_dags_command_test(verbose=False)
|
144
|
+
|
145
|
+
# Create test data
|
146
|
+
self.data = {
|
147
|
+
"username": "testname",
|
148
|
+
"email": "test@test.com",
|
149
|
+
"password": "Testpassword1!",
|
150
|
+
}
|
151
|
+
|
152
|
+
# Create an admin user for testing
|
153
|
+
self.admin_user = UserModel(
|
154
|
+
{
|
155
|
+
"username": "admin",
|
156
|
+
"email": "admin@test.com",
|
157
|
+
"password": "Adminpassword1!",
|
158
|
+
}
|
159
|
+
)
|
160
|
+
self.admin_user.save()
|
161
|
+
|
162
|
+
# Assign admin role to the user
|
163
|
+
admin_role = UserRoleModel(
|
164
|
+
{"user_id": self.admin_user.id, "role_id": ADMIN_ROLE}
|
165
|
+
)
|
166
|
+
admin_role.save()
|
167
|
+
|
168
|
+
# Create a regular user for testing
|
169
|
+
self.regular_user = UserModel(
|
170
|
+
{
|
171
|
+
"username": "regular",
|
172
|
+
"email": "regular@test.com",
|
173
|
+
"password": "Regularpassword1!",
|
174
|
+
}
|
175
|
+
)
|
176
|
+
self.regular_user.save()
|
177
|
+
|
178
|
+
# Assign planner role to the user
|
179
|
+
planner_role = UserRoleModel(
|
180
|
+
{"user_id": self.regular_user.id, "role_id": PLANNER_ROLE}
|
181
|
+
)
|
182
|
+
planner_role.save()
|
183
|
+
|
184
|
+
def tearDown(self):
|
185
|
+
db.session.remove()
|
186
|
+
db.drop_all()
|
187
|
+
|
188
|
+
def get_auth_token(self, user):
|
189
|
+
"""Helper method to get authentication token for a user"""
|
190
|
+
auth = Auth()
|
191
|
+
return auth.generate_token(user.id)
|
192
|
+
|
193
|
+
def test_authenticated_signup_admin_can_register(self):
|
194
|
+
"""Test that admin users can register new users"""
|
195
|
+
payload = self.data
|
196
|
+
admin_token = self.get_auth_token(self.admin_user)
|
197
|
+
|
198
|
+
response = self.client.post(
|
199
|
+
SIGNUP_URL,
|
200
|
+
data=json.dumps(payload),
|
201
|
+
follow_redirects=True,
|
202
|
+
headers={
|
203
|
+
"Content-Type": "application/json",
|
204
|
+
"Authorization": f"Bearer {admin_token}",
|
205
|
+
},
|
206
|
+
)
|
207
|
+
|
208
|
+
self.assertEqual(201, response.status_code)
|
209
|
+
self.assertEqual(str, type(response.json["token"]))
|
210
|
+
self.assertEqual(int, type(response.json["id"]))
|
211
|
+
self.assertEqual(
|
212
|
+
PLANNER_ROLE,
|
213
|
+
UserRoleModel.query.filter_by(user_id=response.json["id"]).first().role_id,
|
214
|
+
)
|
215
|
+
self.assertNotEqual(None, UserModel.get_one_user_by_email(self.data["email"]))
|
216
|
+
|
217
|
+
def test_authenticated_signup_regular_user_cannot_register(self):
|
218
|
+
"""Test that regular users cannot register new users"""
|
219
|
+
payload = self.data
|
220
|
+
regular_token = self.get_auth_token(self.regular_user)
|
221
|
+
|
222
|
+
response = self.client.post(
|
223
|
+
SIGNUP_URL,
|
224
|
+
data=json.dumps(payload),
|
225
|
+
follow_redirects=True,
|
226
|
+
headers={
|
227
|
+
"Content-Type": "application/json",
|
228
|
+
"Authorization": f"Bearer {regular_token}",
|
229
|
+
},
|
230
|
+
)
|
231
|
+
|
232
|
+
# Should return 403 Forbidden due to insufficient permissions
|
233
|
+
self.assertEqual(403, response.status_code)
|
234
|
+
self.assertIn("error", response.json)
|
235
|
+
|
236
|
+
def test_authenticated_signup_no_auth_header_fails(self):
|
237
|
+
"""Test that signup fails without authentication header"""
|
238
|
+
payload = self.data
|
239
|
+
|
240
|
+
response = self.client.post(
|
241
|
+
SIGNUP_URL,
|
242
|
+
data=json.dumps(payload),
|
243
|
+
follow_redirects=True,
|
244
|
+
headers={"Content-Type": "application/json"},
|
245
|
+
)
|
246
|
+
|
247
|
+
# Should return 400 Bad Request due to missing authorization
|
248
|
+
self.assertEqual(400, response.status_code)
|
249
|
+
self.assertIn("error", response.json)
|
250
|
+
|
251
|
+
def test_authenticated_signup_invalid_token_fails(self):
|
252
|
+
"""Test that signup fails with invalid token"""
|
253
|
+
payload = self.data
|
254
|
+
|
255
|
+
response = self.client.post(
|
256
|
+
SIGNUP_URL,
|
257
|
+
data=json.dumps(payload),
|
258
|
+
follow_redirects=True,
|
259
|
+
headers={
|
260
|
+
"Content-Type": "application/json",
|
261
|
+
"Authorization": "Bearer invalid_token",
|
262
|
+
},
|
263
|
+
)
|
264
|
+
|
265
|
+
# Should return 400 Bad Request due to invalid token
|
266
|
+
self.assertEqual(400, response.status_code)
|
267
|
+
self.assertIn("error", response.json)
|
268
|
+
|
269
|
+
def test_signup_permissions_are_registered_in_database(self):
|
270
|
+
"""Test that signup permissions are properly registered in the database"""
|
271
|
+
# Check that the signup endpoint exists in the views table
|
272
|
+
signup_view = ViewModel.query.filter_by(name="signup").first()
|
273
|
+
self.assertIsNotNone(
|
274
|
+
signup_view, "Signup endpoint should be registered in views table"
|
275
|
+
)
|
276
|
+
self.assertEqual("/signup/", signup_view.url_rule)
|
277
|
+
|
278
|
+
# Check that admin role has permissions for signup endpoint
|
279
|
+
admin_permissions = PermissionViewRoleModel.query.filter_by(
|
280
|
+
role_id=ADMIN_ROLE, api_view_id=signup_view.id
|
281
|
+
).all()
|
282
|
+
|
283
|
+
# Admin should have POST permission for signup endpoint
|
284
|
+
self.assertGreater(
|
285
|
+
len(admin_permissions),
|
286
|
+
0,
|
287
|
+
"Admin should have permissions for signup endpoint",
|
288
|
+
)
|
289
|
+
|
290
|
+
# Verify that the permission is for POST action
|
291
|
+
post_permission = PermissionViewRoleModel.query.filter_by(
|
292
|
+
role_id=ADMIN_ROLE, api_view_id=signup_view.id, action_id=POST_ACTION
|
293
|
+
).first()
|
294
|
+
|
295
|
+
self.assertIsNotNone(
|
296
|
+
post_permission, "Admin should have POST permission for signup endpoint"
|
297
|
+
)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cornflow
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.4
|
4
4
|
Summary: cornflow is an open source multi-solver optimization server with a REST API built using flask.
|
5
5
|
Home-page: https://github.com/baobabsoluciones/cornflow
|
6
6
|
Author: baobab soluciones
|
@@ -14,7 +14,7 @@ Requires-Dist: alembic==1.9.2
|
|
14
14
|
Requires-Dist: apispec<=6.3.0
|
15
15
|
Requires-Dist: cachetools==5.3.3
|
16
16
|
Requires-Dist: click<=8.1.7
|
17
|
-
Requires-Dist: cornflow-client
|
17
|
+
Requires-Dist: cornflow-client<=1.2.4
|
18
18
|
Requires-Dist: cryptography<=44.0.1
|
19
19
|
Requires-Dist: disposable-email-domains>=0.0.86
|
20
20
|
Requires-Dist: Flask==2.3.2
|
@@ -37,7 +37,7 @@ Requires-Dist: PuLP<=2.9.0
|
|
37
37
|
Requires-Dist: psycopg2<=2.9.9
|
38
38
|
Requires-Dist: PyJWT<=2.8.0
|
39
39
|
Requires-Dist: pytups>=0.86.2
|
40
|
-
Requires-Dist: requests<=2.32.
|
40
|
+
Requires-Dist: requests<=2.32.4
|
41
41
|
Requires-Dist: SQLAlchemy==1.3.21
|
42
42
|
Requires-Dist: webargs<=8.3.0
|
43
43
|
Requires-Dist: Werkzeug==3.0.6
|