cornflow 1.2.3a3__py3-none-any.whl → 1.2.3a5__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.
@@ -0,0 +1,785 @@
1
+ import unittest
2
+ import os
3
+ import json
4
+ from unittest.mock import patch, MagicMock
5
+ from cornflow.shared import db
6
+ from cornflow.tests.custom_test_case import CustomTestCase
7
+ from cornflow.models import ViewModel
8
+ from cornflow.commands.access import access_init_command
9
+ from cornflow.shared.const import (
10
+ VIEWER_ROLE,
11
+ PLANNER_ROLE,
12
+ POST_ACTION,
13
+ PATCH_ACTION,
14
+ DELETE_ACTION,
15
+ GET_ACTION,
16
+ PUT_ACTION,
17
+ )
18
+
19
+
20
+ class ExternalRoleCreationTestCase(CustomTestCase):
21
+ """
22
+ Test cases for external app custom role creation and removal functionality
23
+ """
24
+
25
+ def _load_expected_permissions(self, test_name):
26
+ """Helper method to load expected permissions from JSON file"""
27
+ test_data_path = os.path.join(
28
+ os.path.dirname(__file__), "..", "data", "expected_permissions.json"
29
+ )
30
+ with open(test_data_path, "r") as f:
31
+ data = json.load(f)
32
+ return [
33
+ (role_id, action_id, endpoint_name)
34
+ for role_id, action_id, endpoint_name in data[test_name]
35
+ ]
36
+
37
+ def _create_mock_external_app_resources(self):
38
+ """Helper method to create mock external app resources"""
39
+ # Create mock endpoint classes for external app
40
+ mock_production_endpoint = MagicMock()
41
+ mock_production_endpoint.ROLES_WITH_ACCESS = [
42
+ 888,
43
+ PLANNER_ROLE,
44
+ ] # Custom role + standard role
45
+ mock_production_endpoint.DESCRIPTION = "Production planning endpoint"
46
+
47
+ mock_quality_endpoint = MagicMock()
48
+ mock_quality_endpoint.ROLES_WITH_ACCESS = [
49
+ 777,
50
+ VIEWER_ROLE,
51
+ ] # Custom role + standard role
52
+ mock_quality_endpoint.DESCRIPTION = "Quality control endpoint"
53
+
54
+ mock_scheduling_endpoint = MagicMock()
55
+ mock_scheduling_endpoint.ROLES_WITH_ACCESS = [
56
+ 888,
57
+ 777,
58
+ PLANNER_ROLE,
59
+ ] # Multiple custom roles
60
+ mock_scheduling_endpoint.DESCRIPTION = "Scheduling optimizer endpoint"
61
+
62
+ # Create mock resources structure for external app endpoints
63
+ mock_resources = [
64
+ {
65
+ "endpoint": "production_planning", # External app endpoint
66
+ "urls": "/production-planning/",
67
+ "resource": mock_production_endpoint,
68
+ },
69
+ {
70
+ "endpoint": "quality_control", # External app endpoint
71
+ "urls": "/quality-control/",
72
+ "resource": mock_quality_endpoint,
73
+ },
74
+ {
75
+ "endpoint": "scheduling_optimizer", # External app endpoint
76
+ "urls": "/scheduling/",
77
+ "resource": mock_scheduling_endpoint,
78
+ },
79
+ ]
80
+
81
+ return mock_resources
82
+
83
+ @patch("cornflow.commands.auxiliar.import_module")
84
+ @patch("cornflow.commands.views.import_module")
85
+ @patch.dict(
86
+ os.environ, {"EXTERNAL_APP": "1", "EXTERNAL_APP_MODULE": "external_test_app"}
87
+ )
88
+ def test_custom_role_creation_removal(
89
+ self, mock_import_views, mock_import_auxiliar
90
+ ):
91
+ """
92
+ Test that custom roles (like role 888) are properly created and removed
93
+ when external app is configured with EXTRA_PERMISSION_ASSIGNATION
94
+ """
95
+ # Mock external app configuration
96
+ mock_external_app = MagicMock()
97
+
98
+ # Mock the shared.const module
99
+ mock_shared = MagicMock()
100
+ mock_const = MagicMock()
101
+ mock_const.EXTRA_PERMISSION_ASSIGNATION = [
102
+ # Try adding an existing permission and it works
103
+ (888, GET_ACTION, "production_planning"),
104
+ # Try adding additional permission
105
+ (888, POST_ACTION, "production_planning"),
106
+ (777, PATCH_ACTION, "quality_control"),
107
+ (VIEWER_ROLE, POST_ACTION, "scheduling_optimizer"),
108
+ (PLANNER_ROLE, DELETE_ACTION, "quality_control"),
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
+ }
117
+ mock_shared.const = mock_const
118
+ mock_external_app.shared = mock_shared
119
+
120
+ # Mock the endpoints.resources with fake external app endpoints
121
+ mock_endpoints = MagicMock()
122
+ mock_endpoints.resources = self._create_mock_external_app_resources()
123
+ mock_external_app.endpoints = mock_endpoints
124
+
125
+ mock_import_views.return_value = mock_external_app
126
+ mock_import_auxiliar.return_value = mock_external_app
127
+
128
+ # Run the permissions registration
129
+ from cornflow.commands.access import access_init_command
130
+
131
+ # Mock the database session for testing
132
+ with patch.object(db.session, "commit"):
133
+ with patch.object(db.session, "rollback"):
134
+ # Run the complete access initialization
135
+ access_init_command(verbose=True)
136
+
137
+ # Verify that custom permissions were created for the external roles
138
+ from cornflow.models import PermissionViewRoleModel
139
+
140
+ # Get all permissions for external app endpoints
141
+ all_permissions = PermissionViewRoleModel.query.all()
142
+ external_permissions = [
143
+ perm
144
+ for perm in all_permissions
145
+ if perm.api_view.name
146
+ in [
147
+ "production_planning",
148
+ "quality_control",
149
+ "scheduling_optimizer",
150
+ ]
151
+ ]
152
+
153
+ # Load expected permissions from JSON file
154
+ expected_permissions = self._load_expected_permissions(
155
+ "test_custom_role_creation_removal"
156
+ )
157
+
158
+ # Verify each expected permission exists
159
+ for role_id, action_id, endpoint_name in expected_permissions:
160
+ permission_exists = any(
161
+ p.role_id == role_id
162
+ and p.action_id == action_id
163
+ and p.api_view.name == endpoint_name
164
+ for p in external_permissions
165
+ )
166
+ self.assertTrue(
167
+ permission_exists,
168
+ f"Expected permission not found: role_id={role_id}, action_id={action_id}, endpoint={endpoint_name}",
169
+ )
170
+
171
+ # Verify we don't have unexpected permissions
172
+ actual_permission_tuples = {
173
+ (p.role_id, p.action_id, p.api_view.name)
174
+ for p in external_permissions
175
+ }
176
+ expected_permission_tuples = set(expected_permissions)
177
+
178
+ unexpected_permissions = (
179
+ actual_permission_tuples - expected_permission_tuples
180
+ )
181
+ self.assertEqual(
182
+ len(unexpected_permissions),
183
+ 0,
184
+ f"Found unexpected permissions: {unexpected_permissions}",
185
+ )
186
+
187
+ missing_permissions = (
188
+ expected_permission_tuples - actual_permission_tuples
189
+ )
190
+ self.assertEqual(
191
+ len(missing_permissions),
192
+ 0,
193
+ f"Missing expected permissions: {missing_permissions}",
194
+ )
195
+
196
+ @patch("cornflow.commands.auxiliar.import_module")
197
+ @patch("cornflow.commands.views.import_module")
198
+ @patch.dict(
199
+ os.environ, {"EXTERNAL_APP": "1", "EXTERNAL_APP_MODULE": "external_test_app"}
200
+ )
201
+ def test_role_removal_when_not_in_config(
202
+ self, mock_import_views, mock_import_auxiliar
203
+ ):
204
+ """
205
+ Test that roles are properly removed when they're no longer in EXTRA_PERMISSION_ASSIGNATION
206
+ """
207
+ # First, create roles with permissions
208
+ mock_external_app = MagicMock()
209
+
210
+ # Mock the shared.const module
211
+ mock_shared = MagicMock()
212
+ mock_const = MagicMock()
213
+ mock_const.EXTRA_PERMISSION_ASSIGNATION = [
214
+ (10000, POST_ACTION, "production_planning"),
215
+ (999, PATCH_ACTION, "quality_control"),
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
+ }
228
+ mock_shared.const = mock_const
229
+ mock_external_app.shared = mock_shared
230
+
231
+ # Mock the endpoints.resources
232
+ mock_endpoints = MagicMock()
233
+ mock_endpoints.resources = self._create_mock_external_app_resources()
234
+ mock_external_app.endpoints = mock_endpoints
235
+
236
+ mock_import_views.return_value = mock_external_app
237
+ mock_import_auxiliar.return_value = mock_external_app
238
+
239
+ # Create initial roles
240
+ access_init_command(verbose=True)
241
+
242
+ # Now update config to remove role 777
243
+ mock_const.EXTRA_PERMISSION_ASSIGNATION = [
244
+ (10000, POST_ACTION, "production_planning"),
245
+ ]
246
+ # Keep the same CUSTOM_ROLES_ACTIONS (role definitions don't change)
247
+
248
+ # Re-run permissions registration
249
+ access_init_command(verbose=True)
250
+
251
+ # Verify role 888 still has permissions but 777 does not
252
+ from cornflow.models import PermissionViewRoleModel
253
+
254
+ permissions_10000 = PermissionViewRoleModel.query.filter_by(role_id=10000).all()
255
+ permissions_999 = PermissionViewRoleModel.query.filter_by(role_id=999).all()
256
+
257
+ self.assertTrue(len(permissions_10000) > 0)
258
+ self.assertEqual(len(permissions_999), 0)
259
+
260
+ def test_fallback_when_no_external_config(self):
261
+ """
262
+ Test that the system falls back gracefully when EXTRA_PERMISSION_ASSIGNATION is not available
263
+ """
264
+ # Should not raise any exceptions
265
+ try:
266
+ access_init_command(verbose=True)
267
+ except Exception as e:
268
+ self.fail(f"access_init_command raised an exception: {e}")
269
+
270
+ @patch("cornflow.commands.auxiliar.import_module")
271
+ @patch("cornflow.commands.views.import_module")
272
+ @patch.dict(
273
+ os.environ, {"EXTERNAL_APP": "1", "EXTERNAL_APP_MODULE": "external_test_app"}
274
+ )
275
+ def test_external_app_missing_extra_permissions(
276
+ self, mock_import_views, mock_import_auxiliar
277
+ ):
278
+ """
279
+ Test graceful handling when external app doesn't have EXTRA_PERMISSION_ASSIGNATION
280
+ """
281
+ # Mock external app without EXTRA_PERMISSION_ASSIGNATION
282
+ mock_external_app = MagicMock()
283
+
284
+ # Mock the shared module but without const.EXTRA_PERMISSION_ASSIGNATION
285
+ mock_shared = MagicMock()
286
+ mock_const = MagicMock()
287
+ # Don't set EXTRA_PERMISSION_ASSIGNATION to trigger AttributeError
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
+ }
296
+ mock_shared.const = mock_const
297
+ mock_external_app.shared = mock_shared
298
+
299
+ # Mock the endpoints.resources
300
+ mock_endpoints = MagicMock()
301
+ mock_endpoints.resources = self._create_mock_external_app_resources()
302
+ mock_external_app.endpoints = mock_endpoints
303
+
304
+ mock_import_views.return_value = mock_external_app
305
+ mock_import_auxiliar.return_value = mock_external_app
306
+
307
+ # Should not raise any exceptions, should fall back gracefully
308
+ try:
309
+ access_init_command(verbose=True)
310
+ except Exception as e:
311
+ self.fail(f"access_init_command raised an exception: {e}")
312
+
313
+ @patch("cornflow.commands.auxiliar.import_module")
314
+ @patch("cornflow.commands.views.import_module")
315
+ @patch.dict(
316
+ os.environ, {"EXTERNAL_APP": "1", "EXTERNAL_APP_MODULE": "external_test_app"}
317
+ )
318
+ def test_standard_role_extended_permissions(
319
+ self, mock_import_views, mock_import_auxiliar
320
+ ):
321
+ """
322
+ Test that standard roles (like VIEWER_ROLE) can get extended permissions from external app
323
+ """
324
+ # Mock external app configuration with extended permissions for existing roles
325
+ mock_external_app = MagicMock()
326
+
327
+ # Mock the shared.const module
328
+ mock_shared = MagicMock()
329
+ mock_const = MagicMock()
330
+ mock_const.EXTRA_PERMISSION_ASSIGNATION = [
331
+ (VIEWER_ROLE, POST_ACTION, "production_planning"), # Extend standard role
332
+ (PLANNER_ROLE, DELETE_ACTION, "quality_control"), # Extend standard role
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
+ }
341
+ mock_shared.const = mock_const
342
+ mock_external_app.shared = mock_shared
343
+
344
+ # Mock the endpoints.resources
345
+ mock_endpoints = MagicMock()
346
+ mock_endpoints.resources = self._create_mock_external_app_resources()
347
+ mock_external_app.endpoints = mock_endpoints
348
+
349
+ mock_import_views.return_value = mock_external_app
350
+ mock_import_auxiliar.return_value = mock_external_app
351
+
352
+ # Mock the database session for testing
353
+ with patch.object(db.session, "commit"):
354
+ with patch.object(db.session, "rollback"):
355
+ # Run the complete access initialization
356
+ access_init_command(verbose=True)
357
+
358
+ # Verify that existing roles got the additional permissions
359
+ from cornflow.models import PermissionViewRoleModel
360
+
361
+ # Check that VIEWER_ROLE has additional permissions
362
+ viewer_permissions = PermissionViewRoleModel.query.filter_by(
363
+ role_id=VIEWER_ROLE
364
+ ).all()
365
+ self.assertTrue(len(viewer_permissions) > 0)
366
+
367
+ # Check that PLANNER_ROLE has additional permissions
368
+ planner_permissions = PermissionViewRoleModel.query.filter_by(
369
+ role_id=PLANNER_ROLE
370
+ ).all()
371
+ self.assertTrue(len(planner_permissions) > 0)
372
+
373
+ @patch("cornflow.commands.auxiliar.import_module")
374
+ @patch("cornflow.commands.views.import_module")
375
+ @patch.dict(
376
+ os.environ, {"EXTERNAL_APP": "1", "EXTERNAL_APP_MODULE": "external_test_app"}
377
+ )
378
+ def test_view_update_and_deletion(self, mock_import_views, mock_import_auxiliar):
379
+ """
380
+ Test that views are updated when URLs change and deleted when resources are removed
381
+ """
382
+ # === INITIAL SETUP ===
383
+ # Create initial mock external app with 3 endpoints
384
+ mock_external_app_initial = MagicMock()
385
+
386
+ # Initial mock endpoints
387
+ mock_production_endpoint = MagicMock()
388
+ mock_production_endpoint.ROLES_WITH_ACCESS = [888]
389
+ mock_production_endpoint.DESCRIPTION = "Production planning endpoint"
390
+
391
+ mock_quality_endpoint = MagicMock()
392
+ mock_quality_endpoint.ROLES_WITH_ACCESS = [777]
393
+ mock_quality_endpoint.DESCRIPTION = "Quality control endpoint"
394
+
395
+ mock_scheduling_endpoint = MagicMock()
396
+ mock_scheduling_endpoint.ROLES_WITH_ACCESS = [888, 777]
397
+ mock_scheduling_endpoint.DESCRIPTION = "Scheduling optimizer endpoint"
398
+
399
+ # Initial resources structure
400
+ initial_resources = [
401
+ {
402
+ "endpoint": "production_planning",
403
+ "urls": "/production-planning/",
404
+ "resource": mock_production_endpoint,
405
+ },
406
+ {
407
+ "endpoint": "quality_control",
408
+ "urls": "/quality-control/",
409
+ "resource": mock_quality_endpoint,
410
+ },
411
+ {
412
+ "endpoint": "scheduling_optimizer",
413
+ "urls": "/scheduling/",
414
+ "resource": mock_scheduling_endpoint,
415
+ },
416
+ ]
417
+
418
+ # Mock the shared.const module (no extra permissions)
419
+ mock_shared_initial = MagicMock()
420
+ mock_const_initial = MagicMock()
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
+ }
429
+ mock_shared_initial.const = mock_const_initial
430
+ mock_external_app_initial.shared = mock_shared_initial
431
+
432
+ # Mock the endpoints.resources
433
+ mock_endpoints_initial = MagicMock()
434
+ mock_endpoints_initial.resources = initial_resources
435
+ mock_external_app_initial.endpoints = mock_endpoints_initial
436
+
437
+ mock_import_views.return_value = mock_external_app_initial
438
+ mock_import_auxiliar.return_value = mock_external_app_initial
439
+
440
+ # === INITIAL ACCESS INIT ===
441
+
442
+ # Mock the database session for testing - INITIAL SETUP ONLY
443
+ with patch.object(db.session, "commit"):
444
+ with patch.object(db.session, "rollback"):
445
+ # Run initial access initialization
446
+ access_init_command(verbose=True)
447
+
448
+ # Verify initial views were created
449
+ initial_views = ViewModel.query.filter(
450
+ ViewModel.name.in_(
451
+ [
452
+ "production_planning",
453
+ "quality_control",
454
+ "scheduling_optimizer",
455
+ ]
456
+ )
457
+ ).all()
458
+
459
+ self.assertEqual(
460
+ len(initial_views), 3, "Expected 3 initial views to be created"
461
+ )
462
+
463
+ # Get initial URLs
464
+ initial_views_dict = {
465
+ view.name: view.url_rule for view in initial_views
466
+ }
467
+ self.assertEqual(
468
+ initial_views_dict["production_planning"], "/production-planning/"
469
+ )
470
+ self.assertEqual(
471
+ initial_views_dict["quality_control"], "/quality-control/"
472
+ )
473
+ self.assertEqual(
474
+ initial_views_dict["scheduling_optimizer"], "/scheduling/"
475
+ )
476
+
477
+ # === MODIFY CONFIGURATION ===
478
+ # Create updated mock external app with:
479
+ # 1. Changed URL for production_planning
480
+ # 2. Changed URL for quality_control
481
+ # 3. Remove scheduling_optimizer entirely
482
+ mock_external_app_updated = MagicMock()
483
+
484
+ # Updated resources structure (scheduling_optimizer removed, URLs changed)
485
+ updated_resources = [
486
+ {
487
+ "endpoint": "production_planning",
488
+ "urls": "/new-production-planning/", # CHANGED URL
489
+ "resource": mock_production_endpoint,
490
+ },
491
+ {
492
+ "endpoint": "quality_control",
493
+ "urls": "/quality-control/",
494
+ "resource": mock_quality_endpoint,
495
+ },
496
+ # scheduling_optimizer REMOVED entirely
497
+ ]
498
+
499
+ # Mock shared const (still no extra permissions)
500
+ mock_shared_updated = MagicMock()
501
+ mock_const_updated = MagicMock()
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
+ }
510
+ mock_shared_updated.const = mock_const_updated
511
+ mock_external_app_updated.shared = mock_shared_updated
512
+
513
+ # Mock updated endpoints.resources
514
+ mock_endpoints_updated = MagicMock()
515
+ mock_endpoints_updated.resources = updated_resources
516
+ mock_external_app_updated.endpoints = mock_endpoints_updated
517
+
518
+ # Update mocks to return updated configuration
519
+ mock_import_views.return_value = mock_external_app_updated
520
+ mock_import_auxiliar.return_value = mock_external_app_updated
521
+
522
+ # === SECOND ACCESS INIT (with updated config) ===
523
+ # Allow real commits for the update to be persisted
524
+ access_init_command(verbose=True)
525
+
526
+ # === VERIFY UPDATES ===
527
+ # Check that URLs were updated
528
+ updated_views = ViewModel.query.filter(
529
+ ViewModel.name.in_(["production_planning", "quality_control"])
530
+ ).all()
531
+
532
+ self.assertEqual(
533
+ len(updated_views), 2, "Expected 2 views to remain after update"
534
+ )
535
+
536
+ updated_views_dict = {view.name: view.url_rule for view in updated_views}
537
+ self.assertEqual(
538
+ updated_views_dict["production_planning"],
539
+ "/new-production-planning/",
540
+ "production_planning URL should be updated",
541
+ )
542
+ self.assertEqual(
543
+ updated_views_dict["quality_control"],
544
+ "/quality-control/",
545
+ "quality_control URL should be updated",
546
+ )
547
+
548
+ # === VERIFY DELETION ===
549
+ # Check that scheduling_optimizer view was deleted
550
+ deleted_view = ViewModel.query.filter_by(name="scheduling_optimizer").first()
551
+ self.assertIsNone(deleted_view, "scheduling_optimizer view should be deleted")
552
+
553
+ # === VERIFY PERMISSIONS ARE CLEANED UP ===
554
+ from cornflow.models import PermissionViewRoleModel
555
+
556
+ # Check that permissions for deleted view are cleaned up
557
+ remaining_permissions = PermissionViewRoleModel.query.all()
558
+ scheduling_permissions = [
559
+ perm
560
+ for perm in remaining_permissions
561
+ if perm.api_view and perm.api_view.name == "scheduling_optimizer"
562
+ ]
563
+ self.assertEqual(
564
+ len(scheduling_permissions),
565
+ 0,
566
+ "All permissions for deleted scheduling_optimizer view should be removed",
567
+ )
568
+
569
+ # Check that permissions for remaining views still exist
570
+ remaining_external_permissions = [
571
+ perm
572
+ for perm in remaining_permissions
573
+ if perm.api_view
574
+ and perm.api_view.name in ["production_planning", "quality_control"]
575
+ ]
576
+
577
+ all_view_names = [
578
+ perm.api_view.name for perm in remaining_permissions if perm.api_view
579
+ ]
580
+ self.assertTrue(
581
+ len(remaining_external_permissions) > 0,
582
+ f"Permissions for remaining views should still exist. Found views: {set(all_view_names)}",
583
+ )
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
+
783
+
784
+ if __name__ == "__main__":
785
+ unittest.main()