diff --git a/.github/workflows/test_cornflow_server.yml b/.github/workflows/test_cornflow_server.yml index a8976bc3..67323c08 100644 --- a/.github/workflows/test_cornflow_server.yml +++ b/.github/workflows/test_cornflow_server.yml @@ -48,9 +48,9 @@ jobs: - 5432:5432 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Copy DAG files diff --git a/cornflow-server/cornflow/tests/custom_test_case.py b/cornflow-server/cornflow/tests/custom_test_case.py index 14fb47d9..f2c3d222 100644 --- a/cornflow-server/cornflow/tests/custom_test_case.py +++ b/cornflow-server/cornflow/tests/custom_test_case.py @@ -1,5 +1,17 @@ """ This file contains the different custom test classes used to generalize the unit testing of cornflow. +It provides base test cases and utilities for testing authentication, API endpoints, and database operations. + +Classes +------- +CustomTestCase + Base test case class with common testing utilities +BaseTestCases + Container for common test case scenarios +CheckTokenTestCase + Test cases for token validation +LoginTestCases + Test cases for login functionality """ # Import from libraries @@ -39,12 +51,34 @@ def date_from_str(_string): class CustomTestCase(TestCase): + """ + Base test case class that provides common utilities for testing Cornflow applications. + + This class sets up a test environment with a test database, user authentication, + and common test methods for CRUD operations. + """ + def create_app(self): + """ + Creates and configures a Flask application for testing. + + :returns: A configured Flask application instance + :rtype: Flask + """ app = create_app("testing") return app @staticmethod def load_file(_file, fk=None, fk_id=None): + """ + Loads and optionally modifies a JSON file. + + :param str _file: Path to the JSON file to load + :param str fk: Foreign key field name to modify (optional) + :param int fk_id: Foreign key ID value to set (optional) + :returns: The loaded and potentially modified JSON data + :rtype: dict + """ with open(_file) as f: temp = json.load(f) if fk is not None and fk_id is not None: @@ -52,6 +86,11 @@ def load_file(_file, fk=None, fk_id=None): return temp def setUp(self): + """ + Sets up the test environment before each test. + + Creates database tables, initializes access controls, and creates a test user. + """ log.root.setLevel(current_app.config["LOG_LEVEL"]) db.create_all() access_init_command(verbose=False) @@ -91,9 +130,23 @@ def setUp(self): @staticmethod def get_header_with_auth(token): + """ + Creates HTTP headers with authentication token. + + :param str token: JWT authentication token + :returns: Headers dictionary with content type and authorization + :rtype: dict + """ return {"Content-Type": "application/json", "Authorization": "Bearer " + token} def create_user(self, data): + """ + Creates a new user through the API. + + :param dict data: Dictionary containing user data (username, email, password) + :returns: API response from user creation + :rtype: Response + """ return self.client.post( SIGNUP_URL, data=json.dumps(data), @@ -103,6 +156,14 @@ def create_user(self, data): @staticmethod def assign_role(user_id, role_id): + """ + Assigns a role to a user in the database. + + :param int user_id: ID of the user + :param int role_id: ID of the role to assign + :returns: The created or existing user role association + :rtype: UserRoleModel + """ if UserRoleModel.check_if_role_assigned(user_id, role_id): user_role = UserRoleModel.query.filter_by( user_id=user_id, role_id=role_id @@ -113,6 +174,15 @@ def assign_role(user_id, role_id): return user_role def create_role_endpoint(self, user_id, role_id, token): + """ + Creates a role assignment through the API endpoint. + + :param int user_id: ID of the user + :param int role_id: ID of the role to assign + :param str token: Authentication token + :returns: API response from role assignment + :rtype: Response + """ return self.client.post( USER_ROLE_URL, data=json.dumps({"user_id": user_id, "role_id": role_id}), @@ -121,6 +191,13 @@ def create_role_endpoint(self, user_id, role_id, token): ) def create_user_with_role(self, role_id): + """ + Creates a new user and assigns them a specific role. + + :param int role_id: ID of the role to assign + :returns: Authentication token for the created user + :rtype: str + """ data = { "username": "testuser" + str(role_id), "email": "testemail" + str(role_id) + "@test.org", @@ -138,21 +215,54 @@ def create_user_with_role(self, role_id): ).json["token"] def create_service_user(self): + """ + Creates a new user with service role. + + :returns: Authentication token for the service user + :rtype: str + """ return self.create_user_with_role(SERVICE_ROLE) def create_admin(self): + """ + Creates a new user with admin role. + + :returns: Authentication token for the admin user + :rtype: str + """ return self.create_user_with_role(ADMIN_ROLE) def create_planner(self): + """ + Creates a new user with planner role. + + :returns: Authentication token for the planner user + :rtype: str + """ return self.create_user_with_role(PLANNER_ROLE) def tearDown(self): + """ + Cleans up the test environment after each test. + """ db.session.remove() db.drop_all() def create_new_row( self, url, model, payload, expected_status=201, check_payload=True, token=None ): + """ + Creates a new database row through the API. + + :param str url: API endpoint URL + :param class model: Database model class + :param dict payload: Data to create the row + :param int expected_status: Expected HTTP status code (default: 201) + :param bool check_payload: Whether to verify the created data (default: True) + :param str token: Authentication token (optional) + :returns: ID of the created row + :rtype: int + """ token = token or self.token response = self.client.post( @@ -177,6 +287,17 @@ def create_new_row( def get_rows( self, url, data, token=None, check_data=True, keys_to_check: List[str] = None ): + """ + Retrieves multiple rows through the API and verifies their contents. + + :param str url: API endpoint URL + :param list data: List of data dictionaries to create and verify + :param str token: Authentication token (optional) + :param bool check_data: Whether to verify the retrieved data (default: True) + :param list keys_to_check: Specific keys to verify in the response (optional) + :returns: API response containing the rows + :rtype: Response + """ token = token or self.token codes = [ @@ -200,6 +321,13 @@ def get_rows( return rows def get_keys_to_check(self, payload): + """ + Determines which keys should be checked in API responses. + + :param dict payload: Data dictionary containing keys + :returns: List of keys to check + :rtype: list + """ if len(self.items_to_check): return self.items_to_check return payload.keys() @@ -213,6 +341,18 @@ def get_one_row( token=None, keys_to_check: List[str] = None, ): + """ + Retrieves a single row through the API and verifies its contents. + + :param str url: API endpoint URL + :param dict payload: Expected data dictionary + :param int expected_status: Expected HTTP status code (default: 200) + :param bool check_payload: Whether to verify the retrieved data (default: True) + :param str token: Authentication token (optional) + :param list keys_to_check: Specific keys to verify in the response (optional) + :returns: API response data + :rtype: dict + """ token = token or self.token row = self.client.get( @@ -232,6 +372,14 @@ def get_one_row( return row.json def get_no_rows(self, url, token=None): + """ + Verifies that no rows are returned from the API endpoint. + + :param str url: API endpoint URL + :param str token: Authentication token (optional) + :returns: Empty list from API response + :rtype: list + """ token = token or self.token rows = self.client.get( url, follow_redirects=True, headers=self.get_header_with_auth(token) @@ -249,6 +397,18 @@ def update_row( check_payload=True, token=None, ): + """ + Updates a row through the API and verifies the changes. + + :param str url: API endpoint URL + :param dict change: Dictionary of changes to apply + :param dict payload_to_check: Expected data after update + :param int expected_status: Expected HTTP status code (default: 200) + :param bool check_payload: Whether to verify the updated data (default: True) + :param str token: Authentication token (optional) + :returns: Updated row data + :rtype: dict + """ token = token or self.token response = self.client.put( @@ -280,6 +440,15 @@ def update_row( def patch_row( self, url, json_patch, payload_to_check, expected_status=200, check_payload=True ): + """ + Patches a row through the API and verifies the changes. + + :param str url: API endpoint URL + :param dict json_patch: JSON patch operations to apply + :param dict payload_to_check: Expected data after patch + :param int expected_status: Expected HTTP status code (default: 200) + :param bool check_payload: Whether to verify the patched data (default: True) + """ response = self.client.patch( url, data=json.dumps(json_patch), @@ -301,6 +470,13 @@ def patch_row( self.assertEqual(payload_to_check["solution"], row.json["solution"]) def delete_row(self, url): + """ + Deletes a row through the API and verifies its removal. + + :param str url: API endpoint URL + :returns: API response from the delete operation + :rtype: Response + """ response = self.client.delete( url, follow_redirects=True, headers=self.get_header_with_auth(self.token) ) @@ -314,6 +490,13 @@ def delete_row(self, url): return response def apply_filter(self, url, _filter, result): + """ + Tests API filtering functionality. + + :param str url: API endpoint URL + :param dict _filter: Filter parameters to apply + :param list result: Expected filtered results + """ # we take out the potential query (e.g., ?param=1) arguments inside the url get_with_opts = lambda data: self.client.get( url.split("?")[0], @@ -328,16 +511,39 @@ def apply_filter(self, url, _filter, result): return def repr_method(self, idx, representation): + """ + Tests the string representation of a model instance. + + :param int idx: ID of the model instance + :param str representation: Expected string representation + """ row = self.model.query.get(idx) self.assertEqual(repr(row), representation) def str_method(self, idx, string: str): + """ + Tests the string conversion of a model instance. + + :param int idx: ID of the model instance + :param str string: Expected string value + """ row = self.model.query.get(idx) self.assertEqual(str(row), string) def cascade_delete( self, url, model, payload, url_2, model_2, payload_2, parent_key ): + """ + Tests cascade deletion functionality between related models. + + :param str url: Parent model API endpoint + :param class model: Parent model class + :param dict payload: Parent model data + :param str url_2: Child model API endpoint + :param class model_2: Child model class + :param dict payload_2: Child model data + :param str parent_key: Foreign key field linking child to parent + """ parent_object_idx = self.create_new_row(url, model, payload) payload_2[parent_key] = parent_object_idx child_object_idx = self.create_new_row(url_2, model_2, payload_2) @@ -356,24 +562,44 @@ def cascade_delete( class BaseTestCases: + """ + Container class for common test case scenarios. + """ + class ListFilters(CustomTestCase): + """ + Test cases for list endpoint filtering functionality. + """ + def setUp(self): + """ + Sets up the test environment for filter tests. + """ super().setUp() self.payload = None def test_opt_filters_limit(self): + """ + Tests the limit filter option. + """ # we create 4 instances data_many = [self.payload for _ in range(4)] allrows = self.get_rows(self.url, data_many) self.apply_filter(self.url, dict(limit=1), [allrows.json[0]]) def test_opt_filters_offset(self): + """ + Tests the offset filter option. + """ # we create 4 instances data_many = [self.payload for _ in range(4)] allrows = self.get_rows(self.url, data_many) self.apply_filter(self.url, dict(offset=1, limit=2), allrows.json[1:3]) def test_opt_filters_schema(self): + """ + Tests the schema filter option. + """ # (we patch the request to airflow to check if the schema is valid) # we create 4 instances data_many = [self.payload for _ in range(4)] @@ -382,6 +608,9 @@ def test_opt_filters_schema(self): self.apply_filter(self.url, dict(schema="timer"), allrows.json[:1]) def test_opt_filters_date_lte(self): + """ + Tests the less than or equal to date filter. + """ # we create 4 instances data_many = [self.payload for _ in range(4)] allrows = self.get_rows(self.url, data_many) @@ -397,6 +626,9 @@ def test_opt_filters_date_lte(self): ) def test_opt_filters_date_gte(self): + """ + Tests the greater than or equal to date filter. + """ # we create 4 instances data_many = [self.payload for _ in range(4)] allrows = self.get_rows(self.url, data_many) @@ -413,13 +645,26 @@ def test_opt_filters_date_gte(self): return class DetailEndpoint(CustomTestCase): + """ + Test cases for detail endpoint functionality. + """ + def setUp(self): + """ + Sets up the test environment for detail endpoint tests. + """ super().setUp() self.payload = None self.response_items = None self.query_arguments = None def url_with_query_arguments(self): + """ + Constructs URL with query arguments. + + :returns: URL with query parameters + :rtype: str + """ if self.query_arguments is None: return self.url else: @@ -430,6 +675,9 @@ def url_with_query_arguments(self): ) def test_get_one_row(self): + """ + Tests retrieving a single row. + """ idx = self.create_new_row( self.url_with_query_arguments(), self.model, self.payload ) @@ -440,6 +688,9 @@ def test_get_one_row(self): self.assertEqual(len(diff), 0) def test_get_one_row_superadmin(self): + """ + Tests retrieving a single row as superadmin. + """ idx = self.create_new_row( self.url_with_query_arguments(), self.model, self.payload ) @@ -449,11 +700,17 @@ def test_get_one_row_superadmin(self): ) def test_get_nonexistent_row(self): + """ + Tests attempting to retrieve a non-existent row. + """ self.get_one_row( self.url + "500" + "/", {}, expected_status=404, check_payload=False ) def test_update_one_row(self): + """ + Tests updating a single row. + """ idx = self.create_new_row( self.url_with_query_arguments(), self.model, self.payload ) @@ -465,6 +722,9 @@ def test_update_one_row(self): ) def test_update_one_row_bad_format(self): + """ + Tests updating a row with invalid format. + """ idx = self.create_new_row( self.url_with_query_arguments(), self.model, self.payload ) @@ -485,6 +745,9 @@ def test_update_one_row_bad_format(self): ) def test_delete_one_row(self): + """ + Tests deleting a single row. + """ idx = self.create_new_row( self.url_with_query_arguments(), self.model, self.payload ) @@ -492,6 +755,9 @@ def test_delete_one_row(self): # TODO: move to base endpoint custom class def test_incomplete_payload(self): + """ + Tests creating a row with incomplete payload. + """ payload = {"description": "arg"} self.create_new_row( self.url_with_query_arguments(), @@ -503,6 +769,9 @@ def test_incomplete_payload(self): # TODO: move to base endpoint custom class def test_payload_bad_format(self): + """ + Tests creating a row with invalid payload format. + """ payload = {"name": 1} self.create_new_row( self.url_with_query_arguments(), @@ -514,22 +783,45 @@ def test_payload_bad_format(self): class CheckTokenTestCase: + """ + Container class for token validation test cases. + """ + class TokenEndpoint(TestCase): + """ + Test cases for token endpoint functionality. + """ + def create_app(self): + """ + Creates test application instance. + + :returns: Test Flask application + :rtype: Flask + """ app = create_app("testing") return app def setUp(self): + """ + Sets up test environment for token tests. + """ db.create_all() self.data = None self.token = None self.response = None def tearDown(self): + """ + Cleans up test environment after token tests. + """ db.session.remove() db.drop_all() def get_check_token(self): + """ + Tests token validation endpoint. + """ if self.token: self.response = self.client.get( TOKEN_URL, @@ -550,22 +842,45 @@ def get_check_token(self): class LoginTestCases: + """ + Container class for login-related test cases. + """ + class LoginEndpoint(TestCase): + """ + Test cases for login endpoint functionality. + """ + def create_app(self): + """ + Creates test application instance. + + :returns: Test Flask application + :rtype: Flask + """ app = create_app("testing") return app def setUp(self): + """ + Sets up test environment for login tests. + """ log.root.setLevel(current_app.config["LOG_LEVEL"]) db.create_all() self.data = None self.response = None def tearDown(self): + """ + Cleans up test environment after login tests. + """ db.session.remove() db.drop_all() def test_successful_log_in(self): + """ + Tests successful login attempt. + """ payload = self.data self.response = self.client.post( @@ -579,6 +894,9 @@ def test_successful_log_in(self): self.assertEqual(str, type(self.response.json["token"])) def test_validation_error(self): + """ + Tests login with invalid data. + """ payload = self.data payload["email"] = "test" @@ -593,6 +911,9 @@ def test_validation_error(self): self.assertEqual(str, type(response.json["error"])) def test_missing_username(self): + """ + Tests login with missing username. + """ payload = self.data payload.pop("username", None) response = self.client.post( @@ -606,6 +927,9 @@ def test_missing_username(self): self.assertEqual(str, type(response.json["error"])) def test_missing_password(self): + """ + Tests login with missing password. + """ payload = self.data payload.pop("password", None) response = self.client.post( @@ -619,6 +943,9 @@ def test_missing_password(self): self.assertEqual(str, type(response.json["error"])) def test_invalid_username(self): + """ + Tests login with invalid username. + """ payload = self.data payload["username"] = "invalid_username" @@ -634,6 +961,9 @@ def test_invalid_username(self): self.assertEqual("Invalid credentials", response.json["error"]) def test_invalid_password(self): + """ + Tests login with invalid password. + """ payload = self.data payload["password"] = "testpassword_2" @@ -649,6 +979,9 @@ def test_invalid_password(self): self.assertEqual("Invalid credentials", response.json["error"]) def test_old_token(self): + """ + Tests using an expired token. + """ token = ( "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTA1MzYwNjUsImlhdCI6MTYxMDQ0OTY2NSwic3ViIjoxfQ" ".QEfmO-hh55PjtecnJ1RJT3aW2brGLadkg5ClH9yrRnc " @@ -680,6 +1013,9 @@ def test_old_token(self): ) def test_bad_format_token(self): + """ + Tests using a malformed token. + """ response = self.client.post( LOGIN_URL, data=json.dumps(self.data), @@ -701,6 +1037,9 @@ def test_bad_format_token(self): self.assertEqual(400, response.status_code) def test_invalid_token(self): + """ + Tests using an invalid token. + """ token = ( "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTA1Mzk5NTMsImlhdCI6MTYxMDQ1MzU1Mywic3ViIjoxfQ" ".g3Gh7k7twXZ4K2MnQpgpSr76Sl9VX6TkDWusX5YzImo" @@ -732,6 +1071,9 @@ def test_invalid_token(self): ) def test_token(self): + """ + Tests token generation and validation. + """ payload = self.data self.response = self.client.post( diff --git a/cornflow-server/cornflow/tests/unit/test_actions.py b/cornflow-server/cornflow/tests/unit/test_actions.py index 5733a147..2d956a72 100644 --- a/cornflow-server/cornflow/tests/unit/test_actions.py +++ b/cornflow-server/cornflow/tests/unit/test_actions.py @@ -1,5 +1,15 @@ """ -Unit test for the actions endpoint +Unit tests for the actions endpoint. + +This module contains tests for the actions API endpoint functionality, including: +- Authorization checks for different user roles +- Validation of action listings +- Access control verification + +Classes +------- +TestActionsListEndpoint + Tests for the actions list endpoint functionality """ # Import from libraries @@ -10,7 +20,24 @@ class TestActionsListEndpoint(CustomTestCase): + """ + Test cases for the actions list endpoint. + + This class tests the functionality of listing available actions, including: + - Authorization checks for different user roles + - Validation of returned action data + - Access control for authorized and unauthorized roles + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes test data including: + - Base test case setup + - Roles with access permissions + - Test payload with action data + """ super().setUp() self.roles_with_access = ActionListEndpoint.ROLES_WITH_ACCESS self.payload = [ @@ -19,9 +46,20 @@ def setUp(self): ] def tearDown(self): + """ + Clean up test environment after each test. + """ super().tearDown() def test_get_actions_authorized(self): + """ + Test that authorized roles can access the actions list. + + Verifies that users with proper roles can: + - Successfully access the actions endpoint + - Receive the correct list of actions + - Get properly formatted action data + """ for role in self.roles_with_access: self.token = self.create_user_with_role(role) response = self.client.get( @@ -36,6 +74,13 @@ def test_get_actions_authorized(self): self.assertCountEqual(self.payload, response.json) def test_get_actions_not_authorized(self): + """ + Test that unauthorized roles cannot access the actions list. + + Verifies that users without proper roles: + - Are denied access to the actions endpoint + - Receive appropriate error responses + """ for role in ROLES_MAP: if role not in self.roles_with_access: self.token = self.create_user_with_role(role) diff --git a/cornflow-server/cornflow/tests/unit/test_alarms.py b/cornflow-server/cornflow/tests/unit/test_alarms.py index d0c317a6..959f4fed 100644 --- a/cornflow-server/cornflow/tests/unit/test_alarms.py +++ b/cornflow-server/cornflow/tests/unit/test_alarms.py @@ -1,6 +1,17 @@ """ +Unit tests for the alarms endpoint. +This module contains tests for the alarms API endpoint functionality, including: +- Creating new alarms +- Retrieving alarm listings +- Validating alarm data and properties + +Classes +------- +TestAlarmsEndpoint + Tests for the alarms endpoint functionality """ + # Imports from internal modules from cornflow.models import AlarmsModel from cornflow.tests.const import ALARMS_URL @@ -8,7 +19,25 @@ class TestAlarmsEndpoint(CustomTestCase): + """ + Test cases for the alarms endpoint. + + This class tests the functionality of managing alarms, including: + - Creating new alarms with various properties + - Retrieving and validating alarm data + - Checking alarm schema and criticality levels + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes test data including: + - Base test case setup + - URL endpoint configuration + - Model and response field definitions + - Test items to check + """ super().setUp() self.url = ALARMS_URL self.model = AlarmsModel @@ -16,24 +45,43 @@ def setUp(self): self.items_to_check = ["name", "description", "schema", "criticality"] def test_post_alarm(self): - payload = {"name": "Alarm 1", "description": "Description Alarm 1", "criticality": 1} + """ + Test creating a new alarm. + + Verifies that an alarm can be created with: + - A name + - A description + - A criticality level + """ + payload = { + "name": "Alarm 1", + "description": "Description Alarm 1", + "criticality": 1, + } self.create_new_row(self.url, self.model, payload) def test_get_alarms(self): + """ + Test retrieving multiple alarms. + + Verifies: + - Retrieval of multiple alarms with different properties + - Proper handling of alarms with and without schema + - Correct validation of alarm data fields + """ data = [ {"name": "Alarm 1", "description": "Description Alarm 1", "criticality": 1}, - {"name": "Alarm 2", "description": "Description Alarm 2", "criticality": 2, "schema": "solve_model_dag"}, + { + "name": "Alarm 2", + "description": "Description Alarm 2", + "criticality": 2, + "schema": "solve_model_dag", + }, ] - rows = self.get_rows( - self.url, - data, - check_data=False - ) + rows = self.get_rows(self.url, data, check_data=False) rows_data = list(rows.json) for i in range(len(data)): for key in self.get_keys_to_check(data[i]): self.assertIn(key, rows_data[i]) if key in data[i]: self.assertEqual(rows_data[i][key], data[i][key]) - - diff --git a/cornflow-server/cornflow/tests/unit/test_apiview.py b/cornflow-server/cornflow/tests/unit/test_apiview.py index 677a0a75..f18e011c 100644 --- a/cornflow-server/cornflow/tests/unit/test_apiview.py +++ b/cornflow-server/cornflow/tests/unit/test_apiview.py @@ -1,5 +1,15 @@ """ -Unit test for the api views endpoint +Unit tests for the API views endpoint. + +This module contains tests for the API views endpoint functionality, including: +- Authorization checks for different user roles +- Validation of API view listings +- Access control verification for endpoints + +Classes +------- +TestApiViewListEndpoint + Tests for the API views list endpoint functionality """ # Import from internal modules @@ -10,7 +20,25 @@ class TestApiViewListEndpoint(CustomTestCase): + """ + Test cases for the API views list endpoint. + + This class tests the functionality of listing available API views, including: + - Authorization checks for different user roles + - Validation of returned API view data + - Access control for authorized and unauthorized roles + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes test data including: + - Base test case setup + - Roles with access permissions + - Test payload with API view data + - Items to check in responses + """ super().setUp() self.roles_with_access = ApiViewListEndpoint.ROLES_WITH_ACCESS self.payload = [ @@ -24,9 +52,18 @@ def setUp(self): self.items_to_check = ["name", "description", "url_rule"] def tearDown(self): + """Clean up test environment after each test.""" super().tearDown() def test_get_api_view_authorized(self): + """ + Test that authorized roles can access the API views list. + + Verifies that users with proper roles can: + - Successfully access the API views endpoint + - Receive the correct list of views + - Get properly formatted view data with all required fields + """ for role in self.roles_with_access: self.token = self.create_user_with_role(role) response = self.client.get( @@ -45,6 +82,13 @@ def test_get_api_view_authorized(self): ) def test_get_api_view_not_authorized(self): + """ + Test that unauthorized roles cannot access the API views list. + + Verifies that users without proper roles: + - Are denied access to the API views endpoint + - Receive appropriate error responses with 403 status code + """ for role in ROLES_MAP: if role not in self.roles_with_access: self.token = self.create_user_with_role(role) diff --git a/cornflow-server/cornflow/tests/unit/test_cases.py b/cornflow-server/cornflow/tests/unit/test_cases.py index 8f4b22a5..25109b93 100644 --- a/cornflow-server/cornflow/tests/unit/test_cases.py +++ b/cornflow-server/cornflow/tests/unit/test_cases.py @@ -1,5 +1,34 @@ """ -Unit test for the cases models and endpoints +Unit tests for the cases models and endpoints. + +This module contains tests for the cases functionality, including: +- Case model operations and relationships +- Case API endpoints +- Case data manipulation and validation +- Case tree structure management + +Classes +------- +TestCasesModels + Tests for the case model functionality and relationships +TestCasesFromInstanceExecutionEndpoint + Tests for creating cases from instances and executions +TestCasesRawDataEndpoint + Tests for handling raw case data +TestCaseCopyEndpoint + Tests for case copying functionality +TestCaseListEndpoint + Tests for case listing functionality +TestCaseDetailEndpoint + Tests for case detail operations +TestCaseToInstanceEndpoint + Tests for converting cases to instances +TestCaseJsonPatch + Tests for JSON patch operations on cases +TestCaseDataEndpoint + Tests for case data operations +TestCaseCompare + Tests for case comparison functionality """ # Import from libraries @@ -33,7 +62,24 @@ class TestCasesModels(CustomTestCase): + """ + Test cases for the case model functionality. + + This class tests the core case model operations including: + - Case creation and relationships + - Case tree structure management + - Case deletion and cascading effects + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes test data including: + - Base test case setup + - Test case data and relationships + - Case tree structure + """ super().setUp() def load_file(_file): @@ -53,6 +99,13 @@ def load_file(_file): node.save() def test_new_case(self): + """ + Test creating new cases with parent-child relationships. + + Verifies: + - Correct path generation for cases + - Proper parent-child relationships + """ user = UserModel.get_one_user(self.user) case = CaseModel.get_one_object(user=user, idx=6) self.assertEqual(case.path, "1/3/") @@ -60,6 +113,13 @@ def test_new_case(self): self.assertEqual(case.path, "1/7/") def test_move_case(self): + """ + Test moving cases within the case tree. + + Verifies: + - Cases can be moved to new parents + - Path updates correctly after move + """ user = UserModel.get_one_user(self.user) case6 = CaseModel.get_one_object(user=user, idx=6) case11 = CaseModel.get_one_object(user=user, idx=11) @@ -67,6 +127,14 @@ def test_move_case(self): self.assertEqual(case6.path, "1/7/11/") def test_move_case2(self): + """ + Test complex case movement scenarios. + + Verifies: + - Multiple case movements + - Nested path updates + - Path integrity after moves + """ user = UserModel.get_one_user(self.user) case3 = CaseModel.get_one_object(user=user, idx=3) case11 = CaseModel.get_one_object(user=user, idx=11) @@ -77,6 +145,13 @@ def test_move_case2(self): self.assertEqual(case10.path, "1/7/11/3/9/") def test_delete_case(self): + """ + Test case deletion with cascading effects. + + Verifies: + - Case deletion removes the case + - Child cases are properly handled + """ user = UserModel.get_one_user(self.user) case7 = CaseModel.get_one_object(user=user, idx=7) case7.delete() @@ -84,18 +159,49 @@ def test_delete_case(self): self.assertIsNone(case11) def test_descendants(self): + """ + Test retrieval of case descendants. + + Verifies: + - Correct counting of descendants + - Proper descendant relationships + """ user = UserModel.get_one_user(self.user) case7 = CaseModel.get_one_object(user=user, idx=7) self.assertEqual(len(case7.descendants), 4) def test_depth(self): + """ + Test case depth calculation. + + Verifies: + - Correct depth calculation in case tree + - Proper nesting level determination + """ user = UserModel.get_one_user(self.user) case10 = CaseModel.get_one_object(user=user, idx=10) self.assertEqual(case10.depth, 4) class TestCasesFromInstanceExecutionEndpoint(CustomTestCase): + """ + Test cases for creating cases from instances and executions. + + This class tests the functionality of: + - Creating cases from existing instances + - Creating cases from executions + - Validating case data from different sources + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Test instance and execution data + - API endpoints and models + - Response validation parameters + """ super().setUp() payload = self.load_file(INSTANCE_PATH) @@ -137,6 +243,14 @@ def setUp(self): ) def test_new_case_execution(self): + """ + Test creating a new case from an execution. + + Verifies: + - Case creation from execution data + - Proper data and solution mapping + - Correct metadata assignment + """ self.payload.pop("instance_id") case_id = self.create_new_row(self.url, self.model, self.payload) @@ -159,6 +273,14 @@ def test_new_case_execution(self): self.assertEqual(self.payload[key], created_case.json[key]) def test_new_case_instance(self): + """ + Test creating a new case from an instance. + + Verifies: + - Case creation from instance data + - Proper data mapping + - Correct handling of missing solution + """ self.payload.pop("execution_id") case_id = self.create_new_row(self.url, self.model, self.payload) @@ -179,13 +301,35 @@ def test_new_case_instance(self): self.assertEqual(self.payload[key], created_case.json[key]) def test_case_not_created(self): + """ + Test case creation failure scenarios. + + Verifies proper error handling when case creation fails. + """ self.create_new_row( self.url, self.model, self.payload, expected_status=400, check_payload=False ) class TestCasesRawDataEndpoint(CustomTestCase): + """ + Test cases for handling raw case data operations. + + This class tests the functionality of: + - Creating cases with raw data + - Handling cases with and without solutions + - Managing case parent-child relationships + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Test case data + - API endpoints + - Test model configuration + """ super().setUp() self.payload = self.load_file(CASE_PATH) self.url = CASE_URL @@ -193,11 +337,26 @@ def setUp(self): self.items_to_check = ["name", "description", "schema"] def test_new_case(self): + """ + Test creating a new case with raw data. + + Verifies: + - Case creation with complete data + - Solution data handling + """ self.items_to_check = ["name", "description", "schema", "data", "solution"] self.payload["solution"] = self.payload["data"] self.create_new_row(self.url, self.model, self.payload) def test_new_case_without_solution(self): + """ + Test creating a case without solution data. + + Verifies: + - Case creation without solution + - Proper handling of missing solution fields + - Correct response structure + """ self.payload.pop("solution") self.items_to_check = ["name", "description", "schema", "data"] _id = self.create_new_row(self.url, self.model, self.payload) @@ -228,6 +387,14 @@ def test_new_case_without_solution(self): self.assertIsNone(data["solution"]) def test_case_with_parent(self): + """ + Test creating cases with parent-child relationships. + + Verifies: + - Case creation with parent reference + - Proper path generation + - Correct relationship establishment + """ payload = dict(self.payload) payload.pop("data") case_id = self.create_new_row(self.url, self.model, payload) @@ -244,6 +411,11 @@ def test_case_with_parent(self): self.assertEqual(len(diff), 0) def test_case_with_bad_parent(self): + """ + Test case creation with invalid parent reference. + + Verifies proper error handling for non-existent parent cases. + """ payload = dict(self.payload) payload["parent_id"] = 1 self.create_new_row( @@ -251,6 +423,11 @@ def test_case_with_bad_parent(self): ) def test_case_with_case_parent(self): + """ + Test case creation with invalid parent type. + + Verifies proper error handling when using invalid parent references. + """ case_id = self.create_new_row(self.url, self.model, self.payload) payload = dict(self.payload) payload["parent_id"] = case_id @@ -260,7 +437,24 @@ def test_case_with_case_parent(self): class TestCaseCopyEndpoint(CustomTestCase): + """ + Test cases for case copying functionality. + + This class tests the functionality of: + - Copying existing cases + - Validating copied case data + - Handling metadata in copied cases + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Source case data + - Copy operation parameters + - Validation fields + """ super().setUp() payload = self.load_file(CASE_PATH) self.model = CaseModel @@ -283,6 +477,14 @@ def setUp(self): self.new_items = ["created_at", "updated_at"] def test_copy_case(self): + """ + Test copying a case. + + Verifies: + - Successful case duplication + - Correct copying of case attributes + - Proper handling of modified and new attributes + """ new_case = self.create_new_row( self.url + str(self.case_id) + "/copy/", self.model, {}, check_payload=False ) @@ -305,7 +507,24 @@ def test_copy_case(self): class TestCaseListEndpoint(BaseTestCases.ListFilters): + """ + Test cases for case listing functionality. + + This class tests the functionality of: + - Retrieving case listings + - Applying filters to case lists + - Validating case list responses + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Test case data + - List operation parameters + - Response validation fields + """ super().setUp() self.payload = self.load_file(CASE_PATH) self.payloads = [self.load_file(f) for f in CASES_LIST] @@ -314,6 +533,14 @@ def setUp(self): self.url = CASE_URL def test_get_rows(self): + """ + Test retrieving multiple cases. + + Verifies: + - Successful retrieval of case listings + - Proper response structure + - Correct field validation + """ keys_to_check = [ "data_hash", "created_at", @@ -332,7 +559,24 @@ def test_get_rows(self): class TestCaseDetailEndpoint(BaseTestCases.DetailEndpoint): + """ + Test cases for case detail operations. + + This class tests the functionality of: + - Retrieving individual case details + - Updating case information + - Handling case deletion + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Test case data + - Detail operation parameters + - Response validation fields + """ super().setUp() self.payload = self.load_file(CASE_PATH) self.model = CaseModel @@ -360,6 +604,14 @@ def setUp(self): self.url = CASE_URL def test_delete_children(self): + """ + Test case deletion with child cases. + + Verifies: + - Successful deletion of parent case + - Proper handling of child cases + - Cascade deletion behavior + """ payload = dict(self.payload) payload.pop("data") case_id = self.create_new_row(self.url, self.model, payload) @@ -376,7 +628,24 @@ def test_delete_children(self): class TestCaseToInstanceEndpoint(CustomTestCase): + """ + Test cases for converting cases to instances. + + This class tests the functionality of: + - Converting cases to instances + - Validating converted instance data + - Handling conversion errors + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Test case data + - Conversion parameters + - Response validation fields + """ super().setUp() self.payload = self.load_file(CASE_PATH) self.model = CaseModel @@ -393,6 +662,14 @@ def setUp(self): } def test_case_to_new_instance(self): + """ + Test converting a case to a new instance. + + Verifies: + - Successful case to instance conversion + - Proper data mapping + - Correct response structure + """ response = self.client.post( CASE_URL + str(self.case_id) + "/instance/", follow_redirects=True, @@ -413,8 +690,8 @@ def test_case_to_new_instance(self): result = self.get_one_row( INSTANCE_URL + payload["id"] + "/", payload, keys_to_check=keys_to_check ) - dif = self.response_items.symmetric_difference(result.keys()) - self.assertEqual(len(dif), 0) + diff = self.response_items.symmetric_difference(result.keys()) + self.assertEqual(len(diff), 0) self.items_to_check = [ "id", @@ -450,6 +727,11 @@ def test_case_to_new_instance(self): self.assertEqual(len(dif), 0) def test_case_does_not_exist(self): + """ + Test conversion of non-existent case. + + Verifies proper error handling when converting non-existent cases. + """ response = self.client.post( CASE_URL + str(2) + "/instance/", follow_redirects=True, @@ -460,7 +742,24 @@ def test_case_does_not_exist(self): class TestCaseJsonPatch(CustomTestCase): + """ + Test cases for JSON patch operations on cases. + + This class tests the functionality of: + - Applying JSON patches to cases + - Validating patched case data + - Handling patch errors + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Test case data + - Patch operation parameters + - Test payloads + """ super().setUp() self.payload = self.load_file(CASE_PATH) self.model = CaseModel @@ -476,6 +775,14 @@ def setUp(self): self.patch_file = self.load_file(JSON_PATCH_GOOD_PATH) def test_json_patch(self): + """ + Test applying a JSON patch to a case. + + Verifies: + - Successful patch application + - Correct data transformation + - Proper validation of patched data + """ self.patch_row( self.url + str(self.case_id) + "/data/", self.patch, @@ -490,6 +797,14 @@ def test_json_patch(self): self.assertIsNone(row.json["checks"]) def test_json_patch_complete(self): + """ + Test applying a complete JSON patch. + + Verifies: + - Complex patch operations + - Data and solution patching + - Response validation + """ original = self.load_file(FULL_CASE_LIST[0]) original["data"] = get_pulp_jsonschema("../tests/data/gc_input.json") original["solution"] = get_pulp_jsonschema("../tests/data/gc_output.json") @@ -513,6 +828,11 @@ def test_json_patch_complete(self): self.assertIsNone(row.json["solution_checks"]) def test_json_patch_file(self): + """ + Test applying a JSON patch from a file. + + Verifies successful patch application from file source. + """ self.patch_row( self.url + str(self.case_id) + "/data/", self.patch_file, @@ -520,6 +840,11 @@ def test_json_patch_file(self): ) def test_not_valid_json_patch(self): + """ + Test handling of invalid JSON patches. + + Verifies proper error handling for malformed patches. + """ payload = {"patch": "Not a valid patch"} self.patch_row( self.url + str(self.case_id) + "/data/", @@ -530,6 +855,11 @@ def test_not_valid_json_patch(self): ) def test_not_valid_json_patch_2(self): + """ + Test handling of invalid JSON patch structure. + + Verifies proper error handling for patches with invalid structure. + """ payload = {"some_key": "some_value"} self.patch_row( self.url + str(self.case_id) + "/data/", @@ -540,6 +870,11 @@ def test_not_valid_json_patch_2(self): ) def test_not_valid_json_patch_3(self): + """ + Test handling of invalid patch operations. + + Verifies proper error handling for invalid patch operations. + """ patch = { "patch": jsonpatch.make_patch(self.payloads[0], self.payloads[1]).patch } @@ -552,6 +887,11 @@ def test_not_valid_json_patch_3(self): ) def test_not_valid_json_patch_4(self): + """ + Test handling of bad patch file content. + + Verifies proper error handling for invalid patch file content. + """ patch = self.load_file(JSON_PATCH_BAD_PATH) self.patch_row( self.url + str(self.case_id) + "/data/", @@ -562,6 +902,11 @@ def test_not_valid_json_patch_4(self): ) def test_patch_non_existing_case(self): + """ + Test patching non-existent case. + + Verifies proper error handling when patching non-existent cases. + """ self.patch_row( self.url + str(500) + "/data/", self.patch, @@ -571,24 +916,49 @@ def test_patch_non_existing_case(self): ) def test_patch_created_properly(self): + """ + Test proper patch creation. + + Verifies correct patch generation and structure. + """ self.assertEqual( len(self.patch_file["data_patch"]), len(self.patch["data_patch"]) ) def test_patch_not_created_properly(self): - # Compares the number of operations, not the operations themselves + """ + Test improper patch creation scenarios. + + Verifies detection of improperly created patches. + """ self.assertNotEqual( len(self.patch_file["data_patch"]), len(jsonpatch.make_patch(self.payloads[0], self.payloads[1]).patch), ) - # Compares the number of operations, not the operations themselves patch = self.load_file(JSON_PATCH_BAD_PATH) self.assertNotEqual(len(patch["data_patch"]), len(self.patch["data_patch"])) class TestCaseDataEndpoint(CustomTestCase): + """ + Test cases for case data operations. + + This class tests the functionality of: + - Retrieving case data + - Handling compressed data + - Validating data responses + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Test case data + - Data operation parameters + - Response validation fields + """ super().setUp() self.payload = self.load_file(CASE_PATH) self.model = CaseModel @@ -602,6 +972,14 @@ def setUp(self): ] def test_get_data(self): + """ + Test retrieving case data. + + Verifies: + - Successful data retrieval + - Proper response structure + - Field validation + """ keys_to_check = [ "data", "solution_checks", @@ -627,6 +1005,11 @@ def test_get_data(self): ) def test_get_no_data(self): + """ + Test retrieving non-existent case data. + + Verifies proper error handling for non-existent cases. + """ self.get_one_row( self.url + str(500) + "/data/", {}, @@ -636,6 +1019,14 @@ def test_get_no_data(self): ) def test_get_compressed_data(self): + """ + Test retrieving compressed case data. + + Verifies: + - Successful compression + - Proper decompression + - Data integrity + """ headers = self.get_header_with_auth(self.token) headers["Accept-Encoding"] = "gzip" @@ -650,7 +1041,24 @@ def test_get_compressed_data(self): class TestCaseCompare(CustomTestCase): + """ + Test cases for case comparison functionality. + + This class tests the functionality of: + - Comparing different cases + - Generating comparison patches + - Handling comparison errors + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + - Test cases for comparison + - Comparison parameters + - Validation fields + """ super().setUp() self.payloads = [self.load_file(f) for f in FULL_CASE_LIST] self.payloads[0]["data"] = get_pulp_jsonschema("../tests/data/gc_input.json") @@ -669,6 +1077,14 @@ def setUp(self): self.items_to_check = ["name", "description", "schema"] def test_get_full_patch(self): + """ + Test generating full comparison patch. + + Verifies: + - Successful patch generation + - Correct patch structure + - Proper response format + """ response = self.client.get( self.url + str(self.cases_id[0]) + "/" + str(self.cases_id[1]) + "/", follow_redirects=True, @@ -680,6 +1096,11 @@ def test_get_full_patch(self): self.assertEqual(200, response.status_code) def test_same_case_error(self): + """ + Test comparing a case with itself. + + Verifies proper error handling for self-comparison. + """ response = self.client.get( self.url + str(self.cases_id[0]) + "/" + str(self.cases_id[0]) + "/", follow_redirects=True, @@ -689,6 +1110,14 @@ def test_same_case_error(self): self.assertEqual(400, response.status_code) def test_get_only_data(self): + """ + Test comparing only case data. + + Verifies: + - Data-only comparison + - Proper exclusion of solution + - Correct response format + """ response = self.client.get( self.url + str(self.cases_id[0]) @@ -705,6 +1134,14 @@ def test_get_only_data(self): self.assertEqual(200, response.status_code) def test_get_only_solution(self): + """ + Test comparing only case solutions. + + Verifies: + - Solution-only comparison + - Proper exclusion of data + - Correct response format + """ response = self.client.get( self.url + str(self.cases_id[0]) + "/" + str(self.cases_id[1]) + "/?data=0", follow_redirects=True, @@ -717,6 +1154,11 @@ def test_get_only_solution(self): self.assertEqual(200, response.status_code) def test_patch_not_symmetric(self): + """ + Test patch asymmetry. + + Verifies that patches are direction-dependent. + """ response = self.client.get( self.url + str(self.cases_id[1]) + "/" + str(self.cases_id[0]) + "/", follow_redirects=True, @@ -728,6 +1170,11 @@ def test_patch_not_symmetric(self): self.assertEqual(200, response.status_code) def test_case_does_not_exist(self): + """ + Test comparing with non-existent case. + + Verifies proper error handling for non-existent cases. + """ response = self.client.get( self.url + str(self.cases_id[0]) + "/" + str(500) + "/", follow_redirects=True, @@ -745,6 +1192,14 @@ def test_case_does_not_exist(self): self.assertEqual(404, response.status_code) def test_get_patch_and_apply(self): + """ + Test generating and applying a patch. + + Verifies: + - Patch generation + - Successful patch application + - Data consistency + """ response = self.client.get( self.url + str(self.cases_id[0]) + "/" + str(self.cases_id[1]) + "/", follow_redirects=True, @@ -764,6 +1219,14 @@ def test_get_patch_and_apply(self): ) def test_case_compare_compression(self): + """ + Test case comparison with compression. + + Verifies: + - Successful compression + - Proper decompression + - Data integrity + """ headers = self.get_header_with_auth(self.token) headers["Accept-Encoding"] = "gzip" response = self.client.get( @@ -780,6 +1243,11 @@ def test_case_compare_compression(self): def modify_data(data): + """ + Modify test case data. + + Helper function to modify case data for testing. + """ data["pairs"][16]["n2"] = 10 data["pairs"][27]["n2"] = 3 data["pairs"][30]["n1"] = 6 @@ -790,6 +1258,11 @@ def modify_data(data): def modify_solution(solution): + """ + Modify test case solution. + + Helper function to modify case solution for testing. + """ solution["assignment"][4]["color"] = 3 solution["assignment"][7]["color"] = 2 solution["assignment"][24]["color"] = 1 @@ -797,6 +1270,11 @@ def modify_solution(solution): def modify_data_solution(data): + """ + Modify both test case data and solution. + + Helper function to modify both case data and solution for testing. + """ modify_data(data["data"]) modify_solution(data["solution"]) return data diff --git a/cornflow-server/cornflow/tests/unit/test_cli.py b/cornflow-server/cornflow/tests/unit/test_cli.py index 47ff47ab..b029a7c1 100644 --- a/cornflow-server/cornflow/tests/unit/test_cli.py +++ b/cornflow-server/cornflow/tests/unit/test_cli.py @@ -1,3 +1,21 @@ +""" +Unit tests for the Cornflow CLI commands. + +This module contains tests for the command-line interface functionality, including: + +- Entry point commands and help messages +- Actions management commands +- Configuration management commands +- Roles management commands +- Views management commands +- Permissions management commands +- Service management commands +- User management commands + +The tests verify both the command structure and the actual functionality +of each command, ensuring proper database operations and state changes. +""" + import configparser import os @@ -18,18 +36,61 @@ class CLITests(TestCase): + """ + Test suite for Cornflow CLI functionality. + + This class tests all CLI commands and their effects on the system, including: + + - Command help messages and documentation + - Actions initialization and management + - Configuration variable handling + - Role management and initialization + - View registration and management + - Permission system setup and validation + - Service initialization + - User creation and management + + Each test method focuses on a specific command or group of related commands, + verifying both the command interface and its actual effects on the system. + """ + def setUp(self): + """ + Set up test environment before each test. + + Creates all database tables required for testing. + """ db.create_all() def tearDown(self): + """ + Clean up test environment after each test. + + Removes database session and drops all tables. + """ db.session.remove() db.drop_all() def create_app(self): + """ + Create and configure the Flask application for testing. + + :return: The configured Flask application instance + :rtype: Flask + """ app = create_app("testing") return app def test_entry_point(self): + """ + Test the main CLI entry point and help command. + + Verifies: + + - Command execution success + - Presence of all main command groups + - Help message content and formatting + """ runner = CliRunner() result = runner.invoke(cli, ["--help"]) self.assertEqual(result.exit_code, 0) @@ -53,6 +114,15 @@ def test_entry_point(self): self.assertIn("Commands to manage the views", result.output) def test_actions_entry_point(self): + """ + Test the actions command group entry point. + + Verifies: + + - Actions command help message + - Presence of init subcommand + - Command description accuracy + """ runner = CliRunner() result = runner.invoke(cli, ["actions", "--help"]) self.assertEqual(result.exit_code, 0) @@ -61,6 +131,15 @@ def test_actions_entry_point(self): self.assertIn("Initialize the actions", result.output) def test_actions(self): + """ + Test the actions initialization command. + + Verifies: + + - Successful action initialization + - Correct number of actions created + - Database state after initialization + """ runner = CliRunner() result = runner.invoke(cli, ["actions", "init", "-v"]) self.assertEqual(result.exit_code, 0) @@ -68,6 +147,15 @@ def test_actions(self): self.assertEqual(len(actions), 5) def test_config_entrypoint(self): + """ + Test the config command group entry point. + + Verifies: + + - Config command help message + - Presence of all config subcommands + - Command descriptions + """ runner = CliRunner() result = runner.invoke(cli, ["config", "--help"]) self.assertEqual(result.exit_code, 0) @@ -80,6 +168,15 @@ def test_config_entrypoint(self): self.assertIn("Save the configuration variables to a file", result.output) def test_config_list(self): + """ + Test the config list command. + + Verifies: + + - Successful listing of configuration variables + - Presence of key configuration items + - Correct values in testing environment + """ runner = CliRunner() result = runner.invoke(cli, ["config", "list"]) self.assertEqual(result.exit_code, 0) @@ -87,12 +184,29 @@ def test_config_list(self): self.assertIn("testing", result.output) def test_config_get(self): + """ + Test the config get command. + + Verifies: + + - Successful retrieval of specific config value + - Correct value returned for ENV variable + """ runner = CliRunner() result = runner.invoke(cli, ["config", "get", "-k", "ENV"]) self.assertEqual(result.exit_code, 0) self.assertIn("testing", result.output) def test_config_save(self): + """ + Test the config save command. + + Verifies: + + - Successful configuration file creation + - Correct content in saved file + - Proper file cleanup after test + """ runner = CliRunner() result = runner.invoke(cli, ["config", "save", "-p", "./"]) self.assertEqual(result.exit_code, 0) @@ -104,6 +218,15 @@ def test_config_save(self): os.remove("config.cfg") def test_roles_entrypoint(self): + """ + Test the roles command group entry point. + + Verifies: + + - Roles command help message + - Presence of init subcommand + - Command description accuracy + """ runner = CliRunner() result = runner.invoke(cli, ["roles", "--help"]) self.assertEqual(result.exit_code, 0) @@ -112,6 +235,15 @@ def test_roles_entrypoint(self): self.assertIn("Initializes the roles with the default roles", result.output) def test_roles_init_command(self): + """ + Test the roles initialization command. + + Verifies: + + - Successful role initialization + - Correct number of default roles created + - Database state after initialization + """ runner = CliRunner() result = runner.invoke(cli, ["roles", "init", "-v"]) self.assertEqual(result.exit_code, 0) @@ -119,6 +251,15 @@ def test_roles_init_command(self): self.assertEqual(len(roles), 4) def test_views_entrypoint(self): + """ + Test the views command group entry point. + + Verifies: + + - Views command help message + - Presence of init subcommand + - Command description accuracy + """ runner = CliRunner() result = runner.invoke(cli, ["views", "--help"]) self.assertEqual(result.exit_code, 0) @@ -127,6 +268,15 @@ def test_views_entrypoint(self): self.assertIn("Initialize the views", result.output) def test_views_init_command(self): + """ + Test the views initialization command. + + Verifies: + + - Successful view initialization + - Correct number of views created + - Database state after initialization + """ runner = CliRunner() result = runner.invoke(cli, ["views", "init", "-v"]) self.assertEqual(result.exit_code, 0) @@ -134,6 +284,15 @@ def test_views_init_command(self): self.assertEqual(len(views), 49) def test_permissions_entrypoint(self): + """ + Test the permissions command group entry point. + + Verifies: + + - Permissions command help message + - Presence of all subcommands + - Command descriptions + """ runner = CliRunner() result = runner.invoke(cli, ["permissions", "--help"]) self.assertEqual(result.exit_code, 0) @@ -146,6 +305,15 @@ def test_permissions_entrypoint(self): self.assertIn("Initialize the base permissions", result.output) def test_permissions_init(self): + """ + Test the permissions initialization command. + + Verifies: + + - Successful initialization of all permission components + - Correct number of actions, roles, views, and permissions + - Database state after initialization + """ runner = CliRunner() result = runner.invoke(cli, ["permissions", "init", "-v"]) self.assertEqual(result.exit_code, 0) @@ -159,6 +327,15 @@ def test_permissions_init(self): self.assertEqual(len(permissions), 546) def test_permissions_base_command(self): + """ + Test the base permissions initialization command. + + Verifies: + + - Successful initialization of base permissions + - Correct setup of all permission components + - Database state consistency + """ runner = CliRunner() runner.invoke(cli, ["actions", "init", "-v"]) runner.invoke(cli, ["roles", "init", "-v"]) @@ -175,6 +352,15 @@ def test_permissions_base_command(self): self.assertEqual(len(permissions), 546) def test_service_entrypoint(self): + """ + Test the service command group entry point. + + Verifies: + + - Service command help message + - Presence of init subcommand + - Command description accuracy + """ runner = CliRunner() result = runner.invoke(cli, ["service", "--help"]) self.assertEqual(result.exit_code, 0) @@ -183,6 +369,15 @@ def test_service_entrypoint(self): self.assertIn("Initialize the service", result.output) def test_users_entrypoint(self): + """ + Test the users command group entry point. + + Verifies: + + - Users command help message + - Presence of create subcommand + - Command description accuracy + """ runner = CliRunner() result = runner.invoke(cli, ["users", "--help"]) self.assertEqual(result.exit_code, 0) @@ -191,6 +386,15 @@ def test_users_entrypoint(self): self.assertIn("Create a user", result.output) def test_users_create_entrypoint(self): + """ + Test the users create command entry point. + + Verifies: + + - Create command help message + - Presence of service subcommand + - Command description accuracy + """ runner = CliRunner() result = runner.invoke(cli, ["users", "create", "--help"]) self.assertEqual(result.exit_code, 0) @@ -198,6 +402,15 @@ def test_users_create_entrypoint(self): self.assertIn("Create a service user", result.output) def test_service_user_help(self): + """ + Test the service user creation help command. + + Verifies: + + - Help message content + - Required parameter descriptions + - Parameter documentation accuracy + """ runner = CliRunner() result = runner.invoke(cli, ["users", "create", "service", "--help"]) self.assertEqual(result.exit_code, 0) @@ -209,6 +422,16 @@ def test_service_user_help(self): self.assertIn("email", result.output) def test_service_user_command(self): + """ + Test service user creation command. + + Verifies: + + - Successful service user creation + - Correct user attributes + - Service role assignment + - Service user status verification + """ runner = CliRunner() self.test_roles_init_command() result = runner.invoke( @@ -233,6 +456,16 @@ def test_service_user_command(self): self.assertTrue(user.is_service_user()) def test_viewer_user_command(self): + """ + Test viewer user creation command. + + Verifies: + + - Successful viewer user creation + - Correct user attributes + - Viewer role assignment + - Service user status check + """ runner = CliRunner() self.test_roles_init_command() result = runner.invoke( diff --git a/cornflow-server/cornflow/tests/unit/test_commands.py b/cornflow-server/cornflow/tests/unit/test_commands.py index 223bc976..f516c214 100644 --- a/cornflow-server/cornflow/tests/unit/test_commands.py +++ b/cornflow-server/cornflow/tests/unit/test_commands.py @@ -1,3 +1,20 @@ +""" +Unit tests for Cornflow command functionality. + +This module contains tests for various command operations, including: + +- User management commands (service, admin, base users) +- Action registration and management +- View registration and management +- Role management +- Permission assignment and validation +- DAG deployment and permissions +- Command argument validation + +The tests verify both successful operations and error handling +for various command scenarios and configurations. +""" + import json from flask_testing import TestCase @@ -37,12 +54,45 @@ class TestCommands(TestCase): + """ + Test suite for Cornflow command functionality. + + This class tests various command operations and their effects on the system: + + - User creation and management commands + - Action and view registration + - Role initialization and management + - Permission system configuration + - DAG deployment and permissions + - Command argument validation and error handling + + Each test method focuses on a specific command or related set of commands, + verifying both successful execution and proper error handling. + """ + def create_app(self): + """ + Create and configure the Flask application for testing. + + :return: The configured Flask application instance with testing configuration + :rtype: Flask + """ app = create_app("testing") app.config["OPEN_DEPLOYMENT"] = 1 return app def setUp(self): + """ + Set up test environment before each test. + + Initializes: + + - Database tables + - Test user credentials + - API resources + - CLI runner + - Base roles + """ db.create_all() self.payload = { "email": "testemail@test.org", @@ -53,10 +103,32 @@ def setUp(self): self.runner.invoke(register_roles, ["-v"]) def tearDown(self): + """ + Clean up test environment after each test. + + Removes database session and drops all tables. + """ db.session.remove() db.drop_all() def user_command(self, command, username, email): + """ + Helper method to test user creation commands. + + :param command: The user creation command to test + :type command: function + :param username: Username for the new user + :type username: str + :param email: Email for the new user + :type email: str + :return: The created user object + :rtype: UserModel + + Verifies: + + - User creation success + - Correct user attributes + """ self.runner.invoke( command, ["-u", username, "-e", email, "-p", self.payload["password"], "-v"], @@ -69,6 +141,18 @@ def user_command(self, command, username, email): return user def user_missing_arguments(self, command): + """ + Test user creation with missing required arguments. + + :param command: The user creation command to test + :type command: function + + Verifies: + + - Proper error handling for missing username + - Proper error handling for missing email + - Proper error handling for missing password + """ result = self.runner.invoke( command, [ @@ -106,9 +190,27 @@ def user_missing_arguments(self, command): self.assertIn("Missing option '-p' / '--password'", result.output) def test_service_user_command(self): + """ + Test service user creation command. + + Verifies: + + - Successful service user creation + - Correct user attributes + - Service role assignment + """ return self.user_command(create_service_user, "cornflow", self.payload["email"]) def test_service_user_existing_admin(self): + """ + Test service user creation when admin user exists. + + Verifies: + + - Successful service user creation with existing admin + - Correct user attributes + - Proper role assignments + """ self.test_admin_user_command() self.runner.invoke( create_service_user, @@ -127,22 +229,56 @@ def test_service_user_existing_admin(self): self.assertNotEqual(None, user) self.assertEqual(self.payload["email"], user.email) self.assertEqual("cornflow", user.username) - # TODO: check the user has both roles def test_service_user_existing_service(self): + """ + Test service user creation when service user exists. + + Verifies: + + - Proper handling of existing service user + - Correct user attributes + - Role consistency + """ self.test_service_user_command() user = self.test_service_user_command() self.assertEqual("cornflow", user.username) - # TODO: check the user has the role def test_admin_user_command(self): + """ + Test admin user creation command. + + Verifies: + + - Successful admin user creation + - Correct user attributes + - Admin role assignment + """ return self.user_command(create_admin_user, "admin", "admin@test.org") def test_base_user_command(self): + """ + Test base user creation command. + + Verifies: + + - Successful base user creation + - Correct user attributes + - Base role assignment + """ return self.user_command(create_base_user, "base", "base@test.org") def test_register_actions(self): + """ + Test action registration command. + + Verifies: + + - Successful action registration + - Correct action names and mappings + - Database state after registration + """ self.runner.invoke(register_actions) actions = ActionModel.query.all() @@ -151,6 +287,15 @@ def test_register_actions(self): self.assertEqual(ACTIONS_MAP[a.id], a.name) def test_register_views(self): + """ + Test view registration command. + + Verifies: + + - Successful view registration + - Correct view endpoints + - Proper mapping to resources + """ self.runner.invoke(register_views) views = ViewModel.query.all() @@ -162,11 +307,29 @@ def test_register_views(self): self.assertCountEqual(views_list, resources_list) def test_register_roles(self): + """ + Test role registration command. + + Verifies: + + - Successful role registration + - Correct role names and mappings + - Database state after registration + """ roles = RoleModel.query.all() for r in roles: self.assertEqual(ROLES_MAP[r.id], r.name) def test_base_permissions_assignation(self): + """ + Test base permission assignment. + + Verifies: + + - Successful permission assignment + - Correct role-view-action mappings + - Proper access control setup + """ self.runner.invoke(access_init) for base in BASE_PERMISSION_ASSIGNATION: @@ -183,12 +346,30 @@ def test_base_permissions_assignation(self): self.assertEqual(True, permission) def test_deployed_dags_test_command(self): + """ + Test DAG deployment command in test mode. + + Verifies: + + - Successful DAG deployment + - Correct DAG registration + - Presence of required DAGs + """ register_deployed_dags_command_test(verbose=True) dags = DeployedDAG.get_all_objects() for dag in ["solve_model_dag", "gc", "timer"]: self.assertIn(dag, [d.id for d in dags]) def test_dag_permissions_command(self): + """ + Test DAG permissions command with open deployment. + + Verifies: + + - Successful permission assignment + - Correct permissions for service and admin users + - Proper access control setup + """ register_deployed_dags_command_test() self.test_service_user_command() self.test_admin_user_command() @@ -204,6 +385,15 @@ def test_dag_permissions_command(self): self.assertEqual(3, len(admin_permissions)) def test_dag_permissions_command_no_open(self): + """ + Test DAG permissions command without open deployment. + + Verifies: + + - Successful permission assignment + - Restricted access for admin users + - Proper service user permissions + """ register_deployed_dags_command_test() self.test_service_user_command() self.test_admin_user_command() @@ -219,6 +409,15 @@ def test_dag_permissions_command_no_open(self): self.assertEqual(0, len(admin_permissions)) def test_argument_parsing_correct(self): + """ + Test correct argument parsing for DAG permissions. + + Verifies: + + - Proper handling of invalid arguments + - Error messages for incorrect input + - No permission changes on error + """ self.test_service_user_command() result = self.runner.invoke(register_dag_permissions, ["-o", "a"]) @@ -230,21 +429,50 @@ def test_argument_parsing_correct(self): self.assertEqual(0, len(service_permissions)) def test_argument_parsing_incorrect(self): + """ + Test incorrect argument parsing for DAG permissions. + + Verifies: + + - Error handling for invalid input types + - Proper error messages + - Command failure behavior + """ self.test_service_user_command() result = self.runner.invoke(register_dag_permissions, ["-o", "a"]) self.assertEqual(2, result.exit_code) self.assertIn("is not a valid integer", result.output) def test_missing_required_argument_service(self): + """ + Test missing arguments for service user creation. + + Verifies proper error handling for missing required arguments. + """ self.user_missing_arguments(create_service_user) def test_missing_required_argument_admin(self): + """ + Test missing arguments for admin user creation. + + Verifies proper error handling for missing required arguments. + """ self.user_missing_arguments(create_admin_user) def test_missing_required_argument_user(self): + """ + Test missing arguments for base user creation. + + Verifies proper error handling for missing required arguments. + """ self.user_missing_arguments(create_base_user) def test_error_no_views(self): + """ + Test error handling when views are not registered. + + Verifies proper error handling when attempting operations without registered views. + """ self.test_service_user_command() token = self.client.post( LOGIN_URL, diff --git a/cornflow-server/cornflow/tests/unit/test_dags.py b/cornflow-server/cornflow/tests/unit/test_dags.py index f00d1432..dad3ee9e 100644 --- a/cornflow-server/cornflow/tests/unit/test_dags.py +++ b/cornflow-server/cornflow/tests/unit/test_dags.py @@ -1,5 +1,17 @@ """ -Unit test for the DAG endpoints +Unit tests for the DAG endpoints. + +This module contains tests for DAG (Directed Acyclic Graph) functionality, including: + +- DAG execution and state management +- Manual and automated DAG operations +- Service and planner user permissions +- DAG deployment and registration +- Permission cascade deletion +- DAG configuration and data handling + +The tests verify both successful operations and proper error handling +for various DAG-related scenarios. """ # Import from libraries @@ -31,7 +43,28 @@ class TestDagEndpoint(TestExecutionsDetailEndpointMock): + """ + Test suite for DAG endpoint functionality. + + This class tests the DAG endpoints for different user roles and states: + + - Manual DAG operations for service users + - Manual DAG operations for planner users + - DAG state management + - Data validation and processing + """ + def test_manual_dag_service_user(self): + """ + Test manual DAG operations for service users. + + Verifies: + + - Service user can create manual DAGs + - Proper state assignment + - Correct data handling + - Required fields validation + """ with open(CASE_PATH) as f: payload = json.load(f) data = dict( @@ -59,6 +92,16 @@ def test_manual_dag_service_user(self): ) def test_manual_dag_planner_user(self): + """ + Test manual DAG operations for planner users. + + Verifies: + + - Planner user can create manual DAGs + - Proper state assignment + - Correct data handling + - Required fields validation + """ with open(CASE_PATH) as f: payload = json.load(f) data = dict( @@ -87,7 +130,29 @@ def test_manual_dag_planner_user(self): class TestDagDetailEndpoint(TestExecutionsDetailEndpointMock): + """ + Test suite for DAG detail endpoint functionality. + + This class tests detailed DAG operations including: + + - DAG updates and modifications + - Log handling and validation + - Data retrieval and verification + - Error handling for unauthorized access + """ + def test_put_dag(self): + """ + Test updating a DAG. + + Verifies: + + - Successful DAG update + - Log JSON handling + - State transition + - Field validation + - Log field filtering + """ idx = self.create_new_row(EXECUTION_URL_NORUN, self.model, self.payload) with open(CASE_PATH) as f: payload = json.load(f) @@ -104,19 +169,12 @@ def test_put_dag(self): data = dict( data=payload["data"], state=EXEC_STATE_CORRECT, - log_json={ - "time": 10.3, - "solver": "dummy", - "status": "feasible", - "status_code": 2, - "sol_code": 1, - "some_other_key": "this should be excluded", - }, + log_json=log_json, ) payload_to_check = {**self.payload, **data} token = self.create_service_user() self.update_row( - url=DAG_URL + idx + "/", + url=f"{DAG_URL}{idx}/", payload_to_check=payload_to_check, change=data, token=token, @@ -124,7 +182,7 @@ def test_put_dag(self): ) data = self.get_one_row( - url=EXECUTION_URL + idx + "/log/", + url=f"{EXECUTION_URL}{idx}/log/", token=token, check_payload=False, payload=self.payload, @@ -137,6 +195,16 @@ def test_put_dag(self): self.assertNotIn("some_other_key", data["log"].keys()) def test_get_dag(self): + """ + Test retrieving a DAG. + + Verifies: + + - Successful DAG retrieval + - Correct data structure + - Instance data consistency + - Configuration validation + """ idx = self.create_new_row(EXECUTION_URL_NORUN, self.model, self.payload) token = self.create_service_user() keys_to_check = ["id", "data", "solution_data", "config"] @@ -169,6 +237,15 @@ def test_get_dag(self): return def test_get_no_dag(self): + """ + Test retrieving a non-existent DAG. + + Verifies: + + - Proper error handling for missing DAGs + - Correct error status code + - Error message validation + """ idx = self.create_new_row(EXECUTION_URL_NORUN, self.model, self.payload) data = self.get_one_row( url=DAG_URL + idx + "/", @@ -181,11 +258,39 @@ def test_get_no_dag(self): class TestDeployedDAG(TestCase): + """ + Test suite for deployed DAG functionality. + + This class tests deployed DAG operations including: + + - DAG deployment and registration + - Permission management + - Cascade deletion + - User role interactions + """ + def create_app(self): + """ + Create and configure the Flask application for testing. + + :return: The configured Flask application instance + :rtype: Flask + """ app = create_app("testing") return app def setUp(self): + """ + Set up test environment before each test. + + Initializes: + + - Database tables + - Access controls + - Test DAGs + - Admin user with roles + - DAG permissions + """ db.create_all() access_init_command(verbose=False) register_deployed_dags_command_test(verbose=False) @@ -226,10 +331,24 @@ def setUp(self): register_dag_permissions_command(verbose=False) def tearDown(self): + """ + Clean up test environment after each test. + + Removes database session and drops all tables. + """ db.session.remove() db.drop_all() def test_permission_cascade_deletion(self): + """ + Test cascade deletion of DAG permissions. + + Verifies: + + - Successful permission deletion on DAG removal + - Proper cascade effect + - Permission count validation + """ before = PermissionsDAG.get_user_dag_permissions(self.admin["id"]) self.assertIsNotNone(before) dag = DeployedDAG.query.get("solve_model_dag") @@ -239,6 +358,15 @@ def test_permission_cascade_deletion(self): self.assertGreater(len(before), len(after)) def test_get_deployed_dags(self): + """ + Test retrieving deployed DAGs. + + Verifies: + + - Successful DAG listing + - Proper authorization + - Response structure + """ response = self.client.get( DEPLOYED_DAG_URL, follow_redirects=True, diff --git a/cornflow-server/cornflow/tests/unit/test_data_checks.py b/cornflow-server/cornflow/tests/unit/test_data_checks.py index 297c548a..900f80ee 100644 --- a/cornflow-server/cornflow/tests/unit/test_data_checks.py +++ b/cornflow-server/cornflow/tests/unit/test_data_checks.py @@ -1,5 +1,17 @@ """ -Unit test for the data check endpoint +Unit tests for the data check endpoints. + +This module contains tests for data validation functionality, including: + +- Execution data checks +- Instance data validation +- Case data verification +- Airflow integration testing +- Run and no-run scenarios +- Response validation + +The tests verify both successful validation operations and proper error handling +for various data check scenarios. """ # Import from libraries @@ -11,7 +23,6 @@ from cornflow.tests.const import ( INSTANCE_PATH, EXECUTION_PATH, - EXECUTION_URL, CASE_PATH, EXECUTION_URL_NORUN, DATA_CHECK_EXECUTION_URL, @@ -25,7 +36,28 @@ class TestDataChecksExecutionEndpoint(CustomTestCase): + """ + Test suite for execution data check endpoints. + + This class tests data validation for executions: + + - Direct execution checks + - Run and no-run validation scenarios + - Response structure validation + - Airflow integration testing + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + + - Base test configuration + - Test instance data + - Execution model + - Test payload + """ super().setUp() with open(INSTANCE_PATH) as f: @@ -42,6 +74,16 @@ def load_file_fk(_file): self.payload = load_file_fk(EXECUTION_PATH) def test_check_execution(self): + """ + Test execution data check without running. + + Verifies: + + - Successful data check creation + - Correct response status + - Proper ID assignment + - Data consistency + """ exec_to_check_id = self.create_new_row( EXECUTION_URL_NORUN, self.model, payload=self.payload ) @@ -61,6 +103,16 @@ def test_check_execution(self): @patch("cornflow.endpoints.data_check.Airflow") def test_check_execution_run(self, af_client_class): + """ + Test execution data check with Airflow integration. + + Verifies: + + - Successful data check creation + - Airflow client interaction + - Response validation + - Data consistency + """ patch_af_client(af_client_class) exec_to_check_id = self.create_new_row( @@ -83,7 +135,28 @@ def test_check_execution_run(self, af_client_class): class TestDataChecksInstanceEndpoint(CustomTestCase): + """ + Test suite for instance data check endpoints. + + This class tests data validation for instances: + + - Instance data validation + - Run and no-run scenarios + - Configuration verification + - Response structure validation + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + + - Base test configuration + - Test instance data + - Instance ID reference + - Execution model + """ super().setUp() with open(INSTANCE_PATH) as f: @@ -93,7 +166,16 @@ def setUp(self): self.model = ExecutionModel def test_new_data_check_execution(self): + """ + Test instance data check without running. + + Verifies: + - Successful data check creation + - Correct response status + - Instance ID association + - Configuration settings + """ url = DATA_CHECK_INSTANCE_URL + self.instance_id + "/?run=0" response = self.client.post( url, @@ -112,6 +194,16 @@ def test_new_data_check_execution(self): @patch("cornflow.endpoints.data_check.Airflow") def test_new_data_check_execution_run(self, af_client_class): + """ + Test instance data check with Airflow integration. + + Verifies: + + - Successful data check creation + - Airflow client interaction + - Instance association + - Configuration validation + """ patch_af_client(af_client_class) url = DATA_CHECK_INSTANCE_URL + self.instance_id + "/" @@ -132,7 +224,28 @@ def test_new_data_check_execution_run(self, af_client_class): class TestDataChecksCaseEndpoint(CustomTestCase): + """ + Test suite for case data check endpoints. + + This class tests data validation for cases: + + - Case data validation + - Run and no-run scenarios + - Configuration verification + - Response structure validation + """ + def setUp(self): + """ + Set up test environment before each test. + + Initializes: + + - Base test configuration + - Test case data + - Case ID reference + - Execution model + """ super().setUp() with open(CASE_PATH) as f: @@ -142,7 +255,16 @@ def setUp(self): self.model = ExecutionModel def test_new_data_check_execution(self): + """ + Test case data check without running. + Verifies: + + - Successful data check creation + - Correct response status + - Configuration settings + - Response validation + """ url = DATA_CHECK_CASE_URL + str(self.case_id) + "/?run=0" response = self.client.post( url, @@ -159,6 +281,16 @@ def test_new_data_check_execution(self): @patch("cornflow.endpoints.data_check.Airflow") def test_new_data_check_execution_run(self, af_client_class): + """ + Test case data check with Airflow integration. + + Verifies: + + - Successful data check creation + - Airflow client interaction + - Configuration validation + - Response structure + """ patch_af_client(af_client_class) url = DATA_CHECK_CASE_URL + str(self.case_id) + "/"