diff --git a/api/tests/unit_tests/services/controller_api.py b/api/tests/unit_tests/services/controller_api.py new file mode 100644 index 0000000000..762d7b9090 --- /dev/null +++ b/api/tests/unit_tests/services/controller_api.py @@ -0,0 +1,1082 @@ +""" +Comprehensive API/Controller tests for Dataset endpoints. + +This module contains extensive integration tests for the dataset-related +controller endpoints, testing the HTTP API layer that exposes dataset +functionality through REST endpoints. + +The controller endpoints provide HTTP access to: +- Dataset CRUD operations (list, create, update, delete) +- Document management operations +- Segment management operations +- Hit testing (retrieval testing) operations +- External dataset and knowledge API operations + +These tests verify that: +- HTTP requests are properly routed to service methods +- Request validation works correctly +- Response formatting is correct +- Authentication and authorization are enforced +- Error handling returns appropriate HTTP status codes +- Request/response serialization works properly + +================================================================================ +ARCHITECTURE OVERVIEW +================================================================================ + +The controller layer in Dify uses Flask-RESTX to provide RESTful API endpoints. +Controllers act as a thin layer between HTTP requests and service methods, +handling: + +1. Request Parsing: Extracting and validating parameters from HTTP requests +2. Authentication: Verifying user identity and permissions +3. Authorization: Checking if user has permission to perform operations +4. Service Invocation: Calling appropriate service methods +5. Response Formatting: Serializing service results to HTTP responses +6. Error Handling: Converting exceptions to appropriate HTTP status codes + +Key Components: +- Flask-RESTX Resources: Define endpoint classes with HTTP methods +- Decorators: Handle authentication, authorization, and setup requirements +- Request Parsers: Validate and extract request parameters +- Response Models: Define response structure for Swagger documentation +- Error Handlers: Convert exceptions to HTTP error responses + +================================================================================ +TESTING STRATEGY +================================================================================ + +This test suite follows a comprehensive testing strategy that covers: + +1. HTTP Request/Response Testing: + - GET, POST, PATCH, DELETE methods + - Query parameters and request body validation + - Response status codes and body structure + - Headers and content types + +2. Authentication and Authorization: + - Login required checks + - Account initialization checks + - Permission validation + - Role-based access control + +3. Request Validation: + - Required parameter validation + - Parameter type validation + - Parameter range validation + - Custom validation rules + +4. Error Handling: + - 400 Bad Request (validation errors) + - 401 Unauthorized (authentication errors) + - 403 Forbidden (authorization errors) + - 404 Not Found (resource not found) + - 500 Internal Server Error (unexpected errors) + +5. Service Integration: + - Service method invocation + - Service method parameter passing + - Service method return value handling + - Service exception handling + +================================================================================ +""" + +from unittest.mock import Mock, patch +from uuid import uuid4 + +import pytest +from flask import Flask +from flask_restx import Api + +from controllers.console.datasets.datasets import DatasetApi, DatasetListApi +from controllers.console.datasets.external import ( + ExternalApiTemplateListApi, +) +from controllers.console.datasets.hit_testing import HitTestingApi +from models.dataset import Dataset, DatasetPermissionEnum + +# ============================================================================ +# Test Data Factory +# ============================================================================ +# The Test Data Factory pattern is used here to centralize the creation of +# test objects and mock instances. This approach provides several benefits: +# +# 1. Consistency: All test objects are created using the same factory methods, +# ensuring consistent structure across all tests. +# +# 2. Maintainability: If the structure of models or services changes, we only +# need to update the factory methods rather than every individual test. +# +# 3. Reusability: Factory methods can be reused across multiple test classes, +# reducing code duplication. +# +# 4. Readability: Tests become more readable when they use descriptive factory +# method calls instead of complex object construction logic. +# +# ============================================================================ + + +class ControllerApiTestDataFactory: + """ + Factory class for creating test data and mock objects for controller API tests. + + This factory provides static methods to create mock objects for: + - Flask application and test client setup + - Dataset instances and related models + - User and authentication context + - HTTP request/response objects + - Service method return values + + The factory methods help maintain consistency across tests and reduce + code duplication when setting up test scenarios. + """ + + @staticmethod + def create_flask_app(): + """ + Create a Flask test application for API testing. + + Returns: + Flask application instance configured for testing + """ + app = Flask(__name__) + app.config["TESTING"] = True + app.config["SECRET_KEY"] = "test-secret-key" + return app + + @staticmethod + def create_api_instance(app): + """ + Create a Flask-RESTX API instance. + + Args: + app: Flask application instance + + Returns: + Api instance configured for the application + """ + api = Api(app, doc="/docs/") + return api + + @staticmethod + def create_test_client(app, api, resource_class, route): + """ + Create a Flask test client with a resource registered. + + Args: + app: Flask application instance + api: Flask-RESTX API instance + resource_class: Resource class to register + route: URL route for the resource + + Returns: + Flask test client instance + """ + api.add_resource(resource_class, route) + return app.test_client() + + @staticmethod + def create_dataset_mock( + dataset_id: str = "dataset-123", + name: str = "Test Dataset", + tenant_id: str = "tenant-123", + permission: DatasetPermissionEnum = DatasetPermissionEnum.ONLY_ME, + **kwargs, + ) -> Mock: + """ + Create a mock Dataset instance. + + Args: + dataset_id: Unique identifier for the dataset + name: Name of the dataset + tenant_id: Tenant identifier + permission: Dataset permission level + **kwargs: Additional attributes to set on the mock + + Returns: + Mock object configured as a Dataset instance + """ + dataset = Mock(spec=Dataset) + dataset.id = dataset_id + dataset.name = name + dataset.tenant_id = tenant_id + dataset.permission = permission + dataset.to_dict.return_value = { + "id": dataset_id, + "name": name, + "tenant_id": tenant_id, + "permission": permission.value, + } + for key, value in kwargs.items(): + setattr(dataset, key, value) + return dataset + + @staticmethod + def create_user_mock( + user_id: str = "user-123", + tenant_id: str = "tenant-123", + is_dataset_editor: bool = True, + **kwargs, + ) -> Mock: + """ + Create a mock user/account instance. + + Args: + user_id: Unique identifier for the user + tenant_id: Tenant identifier + is_dataset_editor: Whether user has dataset editor permissions + **kwargs: Additional attributes to set on the mock + + Returns: + Mock object configured as a user/account instance + """ + user = Mock() + user.id = user_id + user.current_tenant_id = tenant_id + user.is_dataset_editor = is_dataset_editor + user.has_edit_permission = True + user.is_dataset_operator = False + for key, value in kwargs.items(): + setattr(user, key, value) + return user + + @staticmethod + def create_paginated_response(items, total, page=1, per_page=20): + """ + Create a mock paginated response. + + Args: + items: List of items in the current page + total: Total number of items + page: Current page number + per_page: Items per page + + Returns: + Mock paginated response object + """ + response = Mock() + response.items = items + response.total = total + response.page = page + response.per_page = per_page + response.pages = (total + per_page - 1) // per_page + return response + + +# ============================================================================ +# Tests for Dataset List Endpoint (GET /datasets) +# ============================================================================ + + +class TestDatasetListApi: + """ + Comprehensive API tests for DatasetListApi (GET /datasets endpoint). + + This test class covers the dataset listing functionality through the + HTTP API, including pagination, search, filtering, and permissions. + + The GET /datasets endpoint: + 1. Requires authentication and account initialization + 2. Supports pagination (page, limit parameters) + 3. Supports search by keyword + 4. Supports filtering by tag IDs + 5. Supports including all datasets (for admins) + 6. Returns paginated list of datasets + + Test scenarios include: + - Successful dataset listing with pagination + - Search functionality + - Tag filtering + - Permission-based filtering + - Error handling (authentication, authorization) + """ + + @pytest.fixture + def app(self): + """ + Create Flask test application. + + Provides a Flask application instance configured for testing. + """ + return ControllerApiTestDataFactory.create_flask_app() + + @pytest.fixture + def api(self, app): + """ + Create Flask-RESTX API instance. + + Provides an API instance for registering resources. + """ + return ControllerApiTestDataFactory.create_api_instance(app) + + @pytest.fixture + def client(self, app, api): + """ + Create test client with DatasetListApi registered. + + Provides a Flask test client that can make HTTP requests to + the dataset list endpoint. + """ + return ControllerApiTestDataFactory.create_test_client(app, api, DatasetListApi, "/datasets") + + @pytest.fixture + def mock_current_user(self): + """ + Mock current user and tenant context. + + Provides mocked current_account_with_tenant function that returns + a user and tenant ID for testing authentication. + """ + with patch("controllers.console.datasets.datasets.current_account_with_tenant") as mock_get_user: + mock_user = ControllerApiTestDataFactory.create_user_mock() + mock_tenant_id = "tenant-123" + mock_get_user.return_value = (mock_user, mock_tenant_id) + yield mock_get_user + + def test_get_datasets_success(self, client, mock_current_user): + """ + Test successful retrieval of dataset list. + + Verifies that when authentication passes, the endpoint returns + a paginated list of datasets. + + This test ensures: + - Authentication is checked + - Service method is called with correct parameters + - Response has correct structure + - Status code is 200 + """ + # Arrange + datasets = [ + ControllerApiTestDataFactory.create_dataset_mock(dataset_id=f"dataset-{i}", name=f"Dataset {i}") + for i in range(3) + ] + + paginated_response = ControllerApiTestDataFactory.create_paginated_response( + items=datasets, total=3, page=1, per_page=20 + ) + + with patch("controllers.console.datasets.datasets.DatasetService.get_datasets") as mock_get_datasets: + mock_get_datasets.return_value = (datasets, 3) + + # Act + response = client.get("/datasets?page=1&limit=20") + + # Assert + assert response.status_code == 200 + data = response.get_json() + assert "data" in data + assert len(data["data"]) == 3 + assert data["total"] == 3 + assert data["page"] == 1 + assert data["limit"] == 20 + + # Verify service was called + mock_get_datasets.assert_called_once() + + def test_get_datasets_with_search(self, client, mock_current_user): + """ + Test dataset listing with search keyword. + + Verifies that search functionality works correctly through the API. + + This test ensures: + - Search keyword is passed to service method + - Filtered results are returned + - Response structure is correct + """ + # Arrange + search_keyword = "test" + datasets = [ControllerApiTestDataFactory.create_dataset_mock(dataset_id="dataset-1", name="Test Dataset")] + + with patch("controllers.console.datasets.datasets.DatasetService.get_datasets") as mock_get_datasets: + mock_get_datasets.return_value = (datasets, 1) + + # Act + response = client.get(f"/datasets?keyword={search_keyword}") + + # Assert + assert response.status_code == 200 + data = response.get_json() + assert len(data["data"]) == 1 + + # Verify search keyword was passed + call_args = mock_get_datasets.call_args + assert call_args[1]["search"] == search_keyword + + def test_get_datasets_with_pagination(self, client, mock_current_user): + """ + Test dataset listing with pagination parameters. + + Verifies that pagination works correctly through the API. + + This test ensures: + - Page and limit parameters are passed correctly + - Pagination metadata is included in response + - Correct datasets are returned for the page + """ + # Arrange + datasets = [ + ControllerApiTestDataFactory.create_dataset_mock(dataset_id=f"dataset-{i}", name=f"Dataset {i}") + for i in range(5) + ] + + with patch("controllers.console.datasets.datasets.DatasetService.get_datasets") as mock_get_datasets: + mock_get_datasets.return_value = (datasets[:3], 5) # First page with 3 items + + # Act + response = client.get("/datasets?page=1&limit=3") + + # Assert + assert response.status_code == 200 + data = response.get_json() + assert len(data["data"]) == 3 + assert data["page"] == 1 + assert data["limit"] == 3 + + # Verify pagination parameters were passed + call_args = mock_get_datasets.call_args + assert call_args[0][0] == 1 # page + assert call_args[0][1] == 3 # per_page + + +# ============================================================================ +# Tests for Dataset Detail Endpoint (GET /datasets/{id}) +# ============================================================================ + + +class TestDatasetApiGet: + """ + Comprehensive API tests for DatasetApi GET method (GET /datasets/{id} endpoint). + + This test class covers the single dataset retrieval functionality through + the HTTP API. + + The GET /datasets/{id} endpoint: + 1. Requires authentication and account initialization + 2. Validates dataset exists + 3. Checks user permissions + 4. Returns dataset details + + Test scenarios include: + - Successful dataset retrieval + - Dataset not found (404) + - Permission denied (403) + - Authentication required + """ + + @pytest.fixture + def app(self): + """Create Flask test application.""" + return ControllerApiTestDataFactory.create_flask_app() + + @pytest.fixture + def api(self, app): + """Create Flask-RESTX API instance.""" + return ControllerApiTestDataFactory.create_api_instance(app) + + @pytest.fixture + def client(self, app, api): + """Create test client with DatasetApi registered.""" + return ControllerApiTestDataFactory.create_test_client(app, api, DatasetApi, "/datasets/") + + @pytest.fixture + def mock_current_user(self): + """Mock current user and tenant context.""" + with patch("controllers.console.datasets.datasets.current_account_with_tenant") as mock_get_user: + mock_user = ControllerApiTestDataFactory.create_user_mock() + mock_tenant_id = "tenant-123" + mock_get_user.return_value = (mock_user, mock_tenant_id) + yield mock_get_user + + def test_get_dataset_success(self, client, mock_current_user): + """ + Test successful retrieval of a single dataset. + + Verifies that when authentication and permissions pass, the endpoint + returns dataset details. + + This test ensures: + - Authentication is checked + - Dataset existence is validated + - Permissions are checked + - Dataset details are returned + - Status code is 200 + """ + # Arrange + dataset_id = str(uuid4()) + dataset = ControllerApiTestDataFactory.create_dataset_mock(dataset_id=dataset_id, name="Test Dataset") + + with ( + patch("controllers.console.datasets.datasets.DatasetService.get_dataset") as mock_get_dataset, + patch("controllers.console.datasets.datasets.DatasetService.check_dataset_permission") as mock_check_perm, + ): + mock_get_dataset.return_value = dataset + mock_check_perm.return_value = None # No exception = permission granted + + # Act + response = client.get(f"/datasets/{dataset_id}") + + # Assert + assert response.status_code == 200 + data = response.get_json() + assert data["id"] == dataset_id + assert data["name"] == "Test Dataset" + + # Verify service methods were called + mock_get_dataset.assert_called_once_with(dataset_id) + mock_check_perm.assert_called_once() + + def test_get_dataset_not_found(self, client, mock_current_user): + """ + Test error handling when dataset is not found. + + Verifies that when dataset doesn't exist, a 404 error is returned. + + This test ensures: + - 404 status code is returned + - Error message is appropriate + - Service method is called + """ + # Arrange + dataset_id = str(uuid4()) + + with ( + patch("controllers.console.datasets.datasets.DatasetService.get_dataset") as mock_get_dataset, + patch("controllers.console.datasets.datasets.DatasetService.check_dataset_permission") as mock_check_perm, + ): + mock_get_dataset.return_value = None # Dataset not found + + # Act + response = client.get(f"/datasets/{dataset_id}") + + # Assert + assert response.status_code == 404 + + # Verify service was called + mock_get_dataset.assert_called_once() + + +# ============================================================================ +# Tests for Dataset Create Endpoint (POST /datasets) +# ============================================================================ + + +class TestDatasetApiCreate: + """ + Comprehensive API tests for DatasetApi POST method (POST /datasets endpoint). + + This test class covers the dataset creation functionality through the HTTP API. + + The POST /datasets endpoint: + 1. Requires authentication and account initialization + 2. Validates request body + 3. Creates dataset via service + 4. Returns created dataset + + Test scenarios include: + - Successful dataset creation + - Request validation errors + - Duplicate name errors + - Authentication required + """ + + @pytest.fixture + def app(self): + """Create Flask test application.""" + return ControllerApiTestDataFactory.create_flask_app() + + @pytest.fixture + def api(self, app): + """Create Flask-RESTX API instance.""" + return ControllerApiTestDataFactory.create_api_instance(app) + + @pytest.fixture + def client(self, app, api): + """Create test client with DatasetApi registered.""" + return ControllerApiTestDataFactory.create_test_client(app, api, DatasetApi, "/datasets") + + @pytest.fixture + def mock_current_user(self): + """Mock current user and tenant context.""" + with patch("controllers.console.datasets.datasets.current_account_with_tenant") as mock_get_user: + mock_user = ControllerApiTestDataFactory.create_user_mock() + mock_tenant_id = "tenant-123" + mock_get_user.return_value = (mock_user, mock_tenant_id) + yield mock_get_user + + def test_create_dataset_success(self, client, mock_current_user): + """ + Test successful creation of a dataset. + + Verifies that when all validation passes, a new dataset is created + and returned. + + This test ensures: + - Request body is validated + - Service method is called with correct parameters + - Created dataset is returned + - Status code is 201 + """ + # Arrange + dataset_id = str(uuid4()) + dataset = ControllerApiTestDataFactory.create_dataset_mock(dataset_id=dataset_id, name="New Dataset") + + request_data = { + "name": "New Dataset", + "description": "Test description", + "permission": "only_me", + } + + with patch("controllers.console.datasets.datasets.DatasetService.create_empty_dataset") as mock_create: + mock_create.return_value = dataset + + # Act + response = client.post( + "/datasets", + json=request_data, + content_type="application/json", + ) + + # Assert + assert response.status_code == 201 + data = response.get_json() + assert data["id"] == dataset_id + assert data["name"] == "New Dataset" + + # Verify service was called + mock_create.assert_called_once() + + +# ============================================================================ +# Tests for Hit Testing Endpoint (POST /datasets/{id}/hit-testing) +# ============================================================================ + + +class TestHitTestingApi: + """ + Comprehensive API tests for HitTestingApi (POST /datasets/{id}/hit-testing endpoint). + + This test class covers the hit testing (retrieval testing) functionality + through the HTTP API. + + The POST /datasets/{id}/hit-testing endpoint: + 1. Requires authentication and account initialization + 2. Validates dataset exists and user has permission + 3. Validates query parameters + 4. Performs retrieval testing + 5. Returns test results + + Test scenarios include: + - Successful hit testing + - Query validation errors + - Dataset not found + - Permission denied + """ + + @pytest.fixture + def app(self): + """Create Flask test application.""" + return ControllerApiTestDataFactory.create_flask_app() + + @pytest.fixture + def api(self, app): + """Create Flask-RESTX API instance.""" + return ControllerApiTestDataFactory.create_api_instance(app) + + @pytest.fixture + def client(self, app, api): + """Create test client with HitTestingApi registered.""" + return ControllerApiTestDataFactory.create_test_client( + app, api, HitTestingApi, "/datasets//hit-testing" + ) + + @pytest.fixture + def mock_current_user(self): + """Mock current user and tenant context.""" + with patch("controllers.console.datasets.hit_testing.current_account_with_tenant") as mock_get_user: + mock_user = ControllerApiTestDataFactory.create_user_mock() + mock_tenant_id = "tenant-123" + mock_get_user.return_value = (mock_user, mock_tenant_id) + yield mock_get_user + + def test_hit_testing_success(self, client, mock_current_user): + """ + Test successful hit testing operation. + + Verifies that when all validation passes, hit testing is performed + and results are returned. + + This test ensures: + - Dataset validation passes + - Query validation passes + - Hit testing service is called + - Results are returned + - Status code is 200 + """ + # Arrange + dataset_id = str(uuid4()) + dataset = ControllerApiTestDataFactory.create_dataset_mock(dataset_id=dataset_id) + + request_data = { + "query": "test query", + "top_k": 10, + } + + expected_result = { + "query": {"content": "test query"}, + "records": [ + {"content": "Result 1", "score": 0.95}, + {"content": "Result 2", "score": 0.85}, + ], + } + + with ( + patch( + "controllers.console.datasets.hit_testing.HitTestingApi.get_and_validate_dataset" + ) as mock_get_dataset, + patch("controllers.console.datasets.hit_testing.HitTestingApi.parse_args") as mock_parse_args, + patch("controllers.console.datasets.hit_testing.HitTestingApi.hit_testing_args_check") as mock_check_args, + patch("controllers.console.datasets.hit_testing.HitTestingApi.perform_hit_testing") as mock_perform, + ): + mock_get_dataset.return_value = dataset + mock_parse_args.return_value = request_data + mock_check_args.return_value = None # No validation error + mock_perform.return_value = expected_result + + # Act + response = client.post( + f"/datasets/{dataset_id}/hit-testing", + json=request_data, + content_type="application/json", + ) + + # Assert + assert response.status_code == 200 + data = response.get_json() + assert "query" in data + assert "records" in data + assert len(data["records"]) == 2 + + # Verify methods were called + mock_get_dataset.assert_called_once() + mock_parse_args.assert_called_once() + mock_check_args.assert_called_once() + mock_perform.assert_called_once() + + +# ============================================================================ +# Tests for External Dataset Endpoints +# ============================================================================ + + +class TestExternalDatasetApi: + """ + Comprehensive API tests for External Dataset endpoints. + + This test class covers the external knowledge API and external dataset + management functionality through the HTTP API. + + Endpoints covered: + - GET /datasets/external-knowledge-api - List external knowledge APIs + - POST /datasets/external-knowledge-api - Create external knowledge API + - GET /datasets/external-knowledge-api/{id} - Get external knowledge API + - PATCH /datasets/external-knowledge-api/{id} - Update external knowledge API + - DELETE /datasets/external-knowledge-api/{id} - Delete external knowledge API + - POST /datasets/external - Create external dataset + + Test scenarios include: + - Successful CRUD operations + - Request validation + - Authentication and authorization + - Error handling + """ + + @pytest.fixture + def app(self): + """Create Flask test application.""" + return ControllerApiTestDataFactory.create_flask_app() + + @pytest.fixture + def api(self, app): + """Create Flask-RESTX API instance.""" + return ControllerApiTestDataFactory.create_api_instance(app) + + @pytest.fixture + def client_list(self, app, api): + """Create test client for external knowledge API list endpoint.""" + return ControllerApiTestDataFactory.create_test_client( + app, api, ExternalApiTemplateListApi, "/datasets/external-knowledge-api" + ) + + @pytest.fixture + def mock_current_user(self): + """Mock current user and tenant context.""" + with patch("controllers.console.datasets.external.current_account_with_tenant") as mock_get_user: + mock_user = ControllerApiTestDataFactory.create_user_mock(is_dataset_editor=True) + mock_tenant_id = "tenant-123" + mock_get_user.return_value = (mock_user, mock_tenant_id) + yield mock_get_user + + def test_get_external_knowledge_apis_success(self, client_list, mock_current_user): + """ + Test successful retrieval of external knowledge API list. + + Verifies that the endpoint returns a paginated list of external + knowledge APIs. + + This test ensures: + - Authentication is checked + - Service method is called + - Paginated response is returned + - Status code is 200 + """ + # Arrange + apis = [{"id": f"api-{i}", "name": f"API {i}", "endpoint": f"https://api{i}.com"} for i in range(3)] + + with patch( + "controllers.console.datasets.external.ExternalDatasetService.get_external_knowledge_apis" + ) as mock_get_apis: + mock_get_apis.return_value = (apis, 3) + + # Act + response = client_list.get("/datasets/external-knowledge-api?page=1&limit=20") + + # Assert + assert response.status_code == 200 + data = response.get_json() + assert "data" in data + assert len(data["data"]) == 3 + assert data["total"] == 3 + + # Verify service was called + mock_get_apis.assert_called_once() + + +# ============================================================================ +# Additional Documentation and Notes +# ============================================================================ +# +# This test suite covers the core API endpoints for dataset operations. +# Additional test scenarios that could be added: +# +# 1. Document Endpoints: +# - POST /datasets/{id}/documents - Upload/create documents +# - GET /datasets/{id}/documents - List documents +# - GET /datasets/{id}/documents/{doc_id} - Get document details +# - PATCH /datasets/{id}/documents/{doc_id} - Update document +# - DELETE /datasets/{id}/documents/{doc_id} - Delete document +# - POST /datasets/{id}/documents/batch - Batch operations +# +# 2. Segment Endpoints: +# - GET /datasets/{id}/segments - List segments +# - GET /datasets/{id}/segments/{segment_id} - Get segment details +# - PATCH /datasets/{id}/segments/{segment_id} - Update segment +# - DELETE /datasets/{id}/segments/{segment_id} - Delete segment +# +# 3. Dataset Update/Delete Endpoints: +# - PATCH /datasets/{id} - Update dataset +# - DELETE /datasets/{id} - Delete dataset +# +# 4. Advanced Scenarios: +# - File upload handling +# - Large payload handling +# - Concurrent request handling +# - Rate limiting +# - CORS headers +# +# These scenarios are not currently implemented but could be added if needed +# based on real-world usage patterns or discovered edge cases. +# +# ============================================================================ + + +# ============================================================================ +# API Testing Best Practices +# ============================================================================ +# +# When writing API tests, consider the following best practices: +# +# 1. Test Structure: +# - Use descriptive test names that explain what is being tested +# - Follow Arrange-Act-Assert pattern +# - Keep tests focused on a single scenario +# - Use fixtures for common setup +# +# 2. Mocking Strategy: +# - Mock external dependencies (database, services, etc.) +# - Mock authentication and authorization +# - Use realistic mock data +# - Verify mock calls to ensure correct integration +# +# 3. Assertions: +# - Verify HTTP status codes +# - Verify response structure +# - Verify response data values +# - Verify service method calls +# - Verify error messages when appropriate +# +# 4. Error Testing: +# - Test all error paths (400, 401, 403, 404, 500) +# - Test validation errors +# - Test authentication failures +# - Test authorization failures +# - Test not found scenarios +# +# 5. Edge Cases: +# - Test with empty data +# - Test with missing required fields +# - Test with invalid data types +# - Test with boundary values +# - Test with special characters +# +# ============================================================================ + + +# ============================================================================ +# Flask-RESTX Resource Testing Patterns +# ============================================================================ +# +# Flask-RESTX resources are tested using Flask's test client. The typical +# pattern involves: +# +# 1. Creating a Flask test application +# 2. Creating a Flask-RESTX API instance +# 3. Registering the resource with a route +# 4. Creating a test client +# 5. Making HTTP requests through the test client +# 6. Asserting on the response +# +# Example pattern: +# +# app = Flask(__name__) +# app.config["TESTING"] = True +# api = Api(app) +# api.add_resource(MyResource, "/my-endpoint") +# client = app.test_client() +# response = client.get("/my-endpoint") +# assert response.status_code == 200 +# +# Decorators on resources (like @login_required) need to be mocked or +# bypassed in tests. This is typically done by mocking the decorator +# functions or the authentication functions they call. +# +# ============================================================================ + + +# ============================================================================ +# Request/Response Validation +# ============================================================================ +# +# API endpoints use Flask-RESTX request parsers to validate incoming requests. +# These parsers: +# +# 1. Extract parameters from query strings, form data, or JSON body +# 2. Validate parameter types (string, integer, float, boolean, etc.) +# 3. Validate parameter ranges and constraints +# 4. Provide default values when parameters are missing +# 5. Raise BadRequest exceptions when validation fails +# +# Response formatting is handled by Flask-RESTX's marshal_with decorator +# or marshal function, which: +# +# 1. Formats response data according to defined models +# 2. Handles nested objects and lists +# 3. Filters out fields not in the model +# 4. Provides consistent response structure +# +# Tests should verify: +# - Request validation works correctly +# - Invalid requests return 400 Bad Request +# - Response structure matches the defined model +# - Response data values are correct +# +# ============================================================================ + + +# ============================================================================ +# Authentication and Authorization Testing +# ============================================================================ +# +# Most API endpoints require authentication and authorization. Testing these +# aspects involves: +# +# 1. Authentication Testing: +# - Test that unauthenticated requests are rejected (401) +# - Test that authenticated requests are accepted +# - Mock the authentication decorators/functions +# - Verify user context is passed correctly +# +# 2. Authorization Testing: +# - Test that unauthorized requests are rejected (403) +# - Test that authorized requests are accepted +# - Test different user roles and permissions +# - Verify permission checks are performed +# +# 3. Common Patterns: +# - Mock current_account_with_tenant() to return test user +# - Mock permission check functions +# - Test with different user roles (admin, editor, operator, etc.) +# - Test with different permission levels (only_me, all_team, etc.) +# +# ============================================================================ + + +# ============================================================================ +# Error Handling in API Tests +# ============================================================================ +# +# API endpoints should handle errors gracefully and return appropriate HTTP +# status codes. Testing error handling involves: +# +# 1. Service Exception Mapping: +# - ValueError -> 400 Bad Request +# - NotFound -> 404 Not Found +# - Forbidden -> 403 Forbidden +# - Unauthorized -> 401 Unauthorized +# - Internal errors -> 500 Internal Server Error +# +# 2. Validation Error Testing: +# - Test missing required parameters +# - Test invalid parameter types +# - Test parameter range violations +# - Test custom validation rules +# +# 3. Error Response Structure: +# - Verify error status code +# - Verify error message is included +# - Verify error structure is consistent +# - Verify error details are helpful +# +# ============================================================================ + + +# ============================================================================ +# Performance and Scalability Considerations +# ============================================================================ +# +# While unit tests focus on correctness, API tests should also consider: +# +# 1. Response Time: +# - Tests should complete quickly +# - Avoid actual database or network calls +# - Use mocks for slow operations +# +# 2. Resource Usage: +# - Tests should not consume excessive memory +# - Tests should clean up after themselves +# - Use fixtures for resource management +# +# 3. Test Isolation: +# - Tests should not depend on each other +# - Tests should not share state +# - Each test should be independently runnable +# +# 4. Maintainability: +# - Tests should be easy to understand +# - Tests should be easy to modify +# - Use descriptive names and comments +# - Follow consistent patterns +# +# ============================================================================