cornflow 1.2.3a5__py3-none-any.whl → 1.3.0rc1__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 +93 -43
- cornflow/commands/auxiliar.py +4 -1
- cornflow/commands/dag.py +7 -7
- cornflow/commands/permissions.py +13 -7
- cornflow/commands/views.py +3 -0
- cornflow/config.py +26 -6
- cornflow/endpoints/__init__.py +27 -0
- cornflow/endpoints/case.py +37 -21
- cornflow/endpoints/dag.py +5 -5
- cornflow/endpoints/data_check.py +8 -7
- cornflow/endpoints/example_data.py +4 -2
- cornflow/endpoints/execution.py +215 -127
- cornflow/endpoints/health.py +30 -11
- cornflow/endpoints/instance.py +3 -3
- cornflow/endpoints/login.py +9 -2
- cornflow/endpoints/permission.py +1 -2
- cornflow/endpoints/schemas.py +3 -3
- cornflow/endpoints/signup.py +17 -11
- cornflow/migrations/versions/999b98e24225.py +34 -0
- cornflow/migrations/versions/cef1df240b27_.py +34 -0
- cornflow/models/__init__.py +2 -1
- cornflow/models/dag.py +8 -9
- cornflow/models/dag_permissions.py +3 -3
- cornflow/models/execution.py +2 -3
- cornflow/models/permissions.py +1 -0
- cornflow/models/user.py +1 -1
- cornflow/schemas/execution.py +14 -1
- cornflow/schemas/health.py +1 -1
- cornflow/shared/authentication/auth.py +14 -1
- cornflow/shared/authentication/decorators.py +32 -2
- cornflow/shared/const.py +58 -1
- cornflow/shared/exceptions.py +2 -1
- cornflow/tests/base_test_execution.py +798 -0
- cornflow/tests/const.py +1 -0
- cornflow/tests/integration/test_commands.py +2 -2
- cornflow/tests/integration/test_cornflowclient.py +2 -1
- cornflow/tests/unit/test_apiview.py +7 -1
- cornflow/tests/unit/test_cases.py +1 -1
- cornflow/tests/unit/test_cli.py +6 -3
- cornflow/tests/unit/test_commands.py +7 -6
- cornflow/tests/unit/test_dags.py +3 -3
- cornflow/tests/unit/test_example_data.py +1 -1
- cornflow/tests/unit/test_executions.py +115 -535
- cornflow/tests/unit/test_get_resources.py +103 -0
- cornflow/tests/unit/test_health.py +84 -3
- cornflow/tests/unit/test_main_alarms.py +1 -1
- cornflow/tests/unit/test_roles.py +2 -1
- cornflow/tests/unit/test_schema_from_models.py +1 -1
- cornflow/tests/unit/test_schemas.py +1 -1
- cornflow/tests/unit/test_sign_up.py +181 -6
- cornflow/tests/unit/tools.py +93 -10
- {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/METADATA +3 -3
- {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/RECORD +57 -53
- {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/WHEEL +0 -0
- {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/entry_points.txt +0 -0
- {cornflow-1.2.3a5.dist-info → cornflow-1.3.0rc1.dist-info}/top_level.txt +0 -0
@@ -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,10 +1,18 @@
|
|
1
1
|
import os
|
2
|
+
from unittest.mock import patch, MagicMock
|
2
3
|
|
3
4
|
from cornflow.shared import db
|
4
5
|
|
5
6
|
from cornflow.app import create_app
|
6
7
|
from cornflow.commands import access_init_command
|
7
|
-
from cornflow.shared.const import
|
8
|
+
from cornflow.shared.const import (
|
9
|
+
STATUS_HEALTHY,
|
10
|
+
STATUS_UNHEALTHY,
|
11
|
+
CORNFLOW_VERSION,
|
12
|
+
DATABRICKS_BACKEND,
|
13
|
+
AIRFLOW_BACKEND,
|
14
|
+
)
|
15
|
+
from cornflow.shared.exceptions import EndpointNotImplemented
|
8
16
|
from cornflow.tests.const import HEALTH_URL
|
9
17
|
from cornflow.tests.custom_test_case import CustomTestCase
|
10
18
|
|
@@ -28,10 +36,83 @@ class TestHealth(CustomTestCase):
|
|
28
36
|
response = self.client.get(HEALTH_URL)
|
29
37
|
self.assertEqual(200, response.status_code)
|
30
38
|
cf_status = response.json["cornflow_status"]
|
31
|
-
|
39
|
+
backend_status = response.json["backend_status"]
|
32
40
|
cf_version = response.json["cornflow_version"]
|
33
41
|
expected_version = CORNFLOW_VERSION
|
34
42
|
self.assertEqual(str, type(cf_status))
|
35
|
-
self.assertEqual(str, type(
|
43
|
+
self.assertEqual(str, type(backend_status))
|
36
44
|
self.assertEqual(cf_status, STATUS_HEALTHY)
|
37
45
|
self.assertEqual(cf_version, expected_version)
|
46
|
+
|
47
|
+
@patch("cornflow.endpoints.health.Databricks")
|
48
|
+
def test_health_databricks_backend_healthy(self, mock_databricks):
|
49
|
+
"""Test health endpoint with Databricks backend when service is healthy"""
|
50
|
+
self.create_service_user()
|
51
|
+
os.environ["CORNFLOW_SERVICE_USER"] = "testuser4"
|
52
|
+
|
53
|
+
# Mock Databricks client
|
54
|
+
mock_db_client = MagicMock()
|
55
|
+
mock_db_client.is_alive.return_value = True
|
56
|
+
mock_databricks.from_config.return_value = mock_db_client
|
57
|
+
|
58
|
+
# Set Databricks backend
|
59
|
+
with self.app.app_context():
|
60
|
+
self.app.config["CORNFLOW_BACKEND"] = DATABRICKS_BACKEND
|
61
|
+
|
62
|
+
response = self.client.get(HEALTH_URL)
|
63
|
+
|
64
|
+
self.assertEqual(200, response.status_code)
|
65
|
+
cf_status = response.json["cornflow_status"]
|
66
|
+
backend_status = response.json["backend_status"]
|
67
|
+
cf_version = response.json["cornflow_version"]
|
68
|
+
|
69
|
+
self.assertEqual(cf_status, STATUS_HEALTHY)
|
70
|
+
self.assertEqual(backend_status, STATUS_HEALTHY)
|
71
|
+
self.assertEqual(cf_version, CORNFLOW_VERSION)
|
72
|
+
|
73
|
+
# Verify Databricks client was called
|
74
|
+
mock_databricks.from_config.assert_called_once()
|
75
|
+
mock_db_client.is_alive.assert_called_once()
|
76
|
+
|
77
|
+
@patch("cornflow.endpoints.health.Databricks")
|
78
|
+
def test_health_databricks_backend_unhealthy(self, mock_databricks):
|
79
|
+
"""Test health endpoint with Databricks backend when service is unhealthy"""
|
80
|
+
self.create_service_user()
|
81
|
+
os.environ["CORNFLOW_SERVICE_USER"] = "testuser4"
|
82
|
+
|
83
|
+
# Mock Databricks client
|
84
|
+
mock_db_client = MagicMock()
|
85
|
+
mock_db_client.is_alive.return_value = False
|
86
|
+
mock_databricks.from_config.return_value = mock_db_client
|
87
|
+
|
88
|
+
# Set Databricks backend
|
89
|
+
with self.app.app_context():
|
90
|
+
self.app.config["CORNFLOW_BACKEND"] = DATABRICKS_BACKEND
|
91
|
+
|
92
|
+
response = self.client.get(HEALTH_URL)
|
93
|
+
|
94
|
+
self.assertEqual(200, response.status_code)
|
95
|
+
cf_status = response.json["cornflow_status"]
|
96
|
+
backend_status = response.json["backend_status"]
|
97
|
+
|
98
|
+
self.assertEqual(cf_status, STATUS_HEALTHY)
|
99
|
+
self.assertEqual(backend_status, STATUS_UNHEALTHY)
|
100
|
+
|
101
|
+
# Verify Databricks client was called
|
102
|
+
mock_databricks.from_config.assert_called_once()
|
103
|
+
mock_db_client.is_alive.assert_called_once()
|
104
|
+
|
105
|
+
def test_health_unsupported_backend(self):
|
106
|
+
"""Test health endpoint with unsupported backend raises EndpointNotImplemented"""
|
107
|
+
self.create_service_user()
|
108
|
+
os.environ["CORNFLOW_SERVICE_USER"] = "testuser4"
|
109
|
+
|
110
|
+
# Set an unsupported backend value
|
111
|
+
with self.app.app_context():
|
112
|
+
self.app.config["CORNFLOW_BACKEND"] = 999 # Invalid backend value
|
113
|
+
|
114
|
+
response = self.client.get(HEALTH_URL)
|
115
|
+
|
116
|
+
self.assertEqual(501, response.status_code)
|
117
|
+
self.assertIn("error", response.json)
|
118
|
+
self.assertEqual(response.json["error"], "Endpoint not implemented")
|
@@ -5,7 +5,7 @@
|
|
5
5
|
import json
|
6
6
|
|
7
7
|
# Imports from internal modules
|
8
|
-
from cornflow.models import MainAlarmsModel
|
8
|
+
from cornflow.models import MainAlarmsModel
|
9
9
|
from cornflow.tests.const import MAIN_ALARMS_URL, ALARMS_URL
|
10
10
|
from cornflow.tests.custom_test_case import CustomTestCase
|
11
11
|
|
@@ -251,7 +251,8 @@ class TestUserRolesListEndpoint(CustomTestCase):
|
|
251
251
|
},
|
252
252
|
)
|
253
253
|
self.assertEqual(200, response.status_code)
|
254
|
-
self.
|
254
|
+
self.assertCountEqual(self.payload, response.json)
|
255
|
+
|
255
256
|
|
256
257
|
def test_get_user_roles_not_authorized_user(self):
|
257
258
|
for role in ROLES_MAP:
|
@@ -81,7 +81,7 @@
|
|
81
81
|
# }
|
82
82
|
# required_instance = {"id", "name", "data_hash"}
|
83
83
|
# foreign_keys = [
|
84
|
-
# ("permission_dag", "dag_id", "
|
84
|
+
# ("permission_dag", "dag_id", "deployed_workflows.id"),
|
85
85
|
# ("permission_dag", "user_id", "users.id"),
|
86
86
|
# ("permission_view", "action_id", "actions.id"),
|
87
87
|
# ("permission_view", "api_view_id", "api_view.id"),
|
@@ -189,7 +189,7 @@ class TestNewSchemaEndpointOpen(CustomTestCase):
|
|
189
189
|
|
190
190
|
def test_get_all_schemas(self):
|
191
191
|
schemas = self.get_one_row(self.url, {}, 200, False, keys_to_check=["name"])
|
192
|
-
dags = [{"name": dag} for dag in ["solve_model_dag", "gc", "timer"]]
|
192
|
+
dags = [{"name": dag} for dag in ["solve_model_dag", "gc", "timer", "979073949072767"]]
|
193
193
|
|
194
194
|
self.assertEqual(dags, schemas)
|
195
195
|
|
@@ -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
|
+
)
|
cornflow/tests/unit/tools.py
CHANGED
@@ -1,13 +1,96 @@
|
|
1
|
-
from unittest.mock import Mock
|
1
|
+
from unittest.mock import Mock, patch
|
2
|
+
from flask import Flask
|
3
|
+
import json
|
4
|
+
import os
|
5
|
+
|
6
|
+
from cornflow.shared.const import (
|
7
|
+
DATABRICKS_TERMINATE_STATE,
|
8
|
+
DATABRICKS_FINISH_TO_STATE_MAP,
|
9
|
+
)
|
10
|
+
|
11
|
+
|
12
|
+
def create_test_app():
|
13
|
+
app = Flask(__name__)
|
14
|
+
app.config["CORNFLOW_BACKEND"] = 2
|
15
|
+
return app
|
2
16
|
|
3
17
|
|
4
18
|
def patch_af_client(af_client_class):
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
19
|
+
with patch("cornflow.endpoints.execution.current_app.config") as mock_config:
|
20
|
+
mock_config.__getitem__.side_effect = lambda key: (
|
21
|
+
1 if key == "CORNFLOW_BACKEND" else {}
|
22
|
+
)
|
23
|
+
af_client_mock = Mock()
|
24
|
+
responses_mock = Mock()
|
25
|
+
responses_mock.json.return_value = {
|
26
|
+
"is_paused": False,
|
27
|
+
"dag_run_id": "12345",
|
28
|
+
"state": "success",
|
29
|
+
}
|
30
|
+
af_client_mock.is_alive.return_value = True
|
31
|
+
af_client_mock.get_workflow_info.return_value = responses_mock
|
32
|
+
|
33
|
+
# Configurar get_workflow_info
|
34
|
+
schema_info_mock = Mock()
|
35
|
+
schema_info_mock.json.return_value = {"is_paused": False}
|
36
|
+
af_client_mock.get_workflow_info.return_value = schema_info_mock
|
37
|
+
|
38
|
+
# Configurar run_workflow
|
39
|
+
run_response_mock = Mock()
|
40
|
+
run_response_mock.json.return_value = {"dag_run_id": "12345"}
|
41
|
+
af_client_mock.run_workflow.return_value = run_response_mock
|
42
|
+
|
43
|
+
# Configurar get_run_status
|
44
|
+
status_mock = Mock()
|
45
|
+
status_mock.json.return_value = {"state": "success"}
|
46
|
+
af_client_mock.get_run_status.return_value = status_mock
|
47
|
+
|
48
|
+
af_client_mock.run_dag.return_value = responses_mock
|
49
|
+
af_client_mock.set_dag_run_to_fail.return_value = None
|
50
|
+
af_client_class.from_config.return_value = af_client_mock
|
51
|
+
|
52
|
+
|
53
|
+
def patch_db_client(db_client_class):
|
54
|
+
mock_config = {"CORNFLOW_BACKEND": 2}
|
55
|
+
|
56
|
+
with patch("cornflow.endpoints.execution.current_app.config", mock_config):
|
57
|
+
db_client_mock = Mock()
|
58
|
+
responses_mock = Mock()
|
59
|
+
responses_mock.json.return_value = {
|
60
|
+
"status": {"state": "SUCCESS", "termination_details": {"code": "SUCCESS"}}
|
61
|
+
}
|
62
|
+
response_run_workflow = Mock()
|
63
|
+
response_run_workflow.json.return_value = {
|
64
|
+
"run_id": 350148078719367,
|
65
|
+
"number_in_job": 350148078719367,
|
66
|
+
}
|
67
|
+
response_get_workflow_info = Mock()
|
68
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
69
|
+
test_data_dir = os.path.join(os.path.dirname(current_dir), "data")
|
70
|
+
with open(
|
71
|
+
os.path.join(test_data_dir, "patch_databricks_get_orch_info.json")
|
72
|
+
) as f:
|
73
|
+
response_get_workflow_info.json.return_value = json.load(f)
|
74
|
+
response_get_run_status = Mock()
|
75
|
+
with open(
|
76
|
+
os.path.join(test_data_dir, "patch_databricks_get_run_status.json")
|
77
|
+
) as f:
|
78
|
+
response_get_run_status.json.return_value = json.load(f)
|
79
|
+
state = response_get_run_status.json.return_value["status"]["state"]
|
80
|
+
if state == DATABRICKS_TERMINATE_STATE:
|
81
|
+
if (
|
82
|
+
response_get_run_status.json.return_value["status"][
|
83
|
+
"termination_details"
|
84
|
+
]["code"]
|
85
|
+
in DATABRICKS_FINISH_TO_STATE_MAP.keys()
|
86
|
+
):
|
87
|
+
response_get_run_status = response_get_run_status.json.return_value[
|
88
|
+
"status"
|
89
|
+
]["termination_details"]["code"]
|
90
|
+
else:
|
91
|
+
response_get_run_status = "OTHER_FINISH_ERROR"
|
92
|
+
db_client_mock.is_alive.return_value = True
|
93
|
+
db_client_mock.get_workflow_info.return_value = response_get_workflow_info
|
94
|
+
db_client_mock.run_workflow.return_value = response_run_workflow
|
95
|
+
db_client_mock.get_run_status.return_value = response_get_run_status
|
96
|
+
db_client_class.from_config.return_value = db_client_mock
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cornflow
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.3.0rc1
|
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.3.0rc1
|
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
|