From 75cb4eec39d67d6268fcd8ac57f2a20a5cdb10ca Mon Sep 17 00:00:00 2001 From: Krrish Ghimire Date: Thu, 16 Apr 2026 21:24:53 +0545 Subject: [PATCH 1/3] test serializers --- backend/core/serializers/configure_app.py | 13 +- .../version_control_serializers.py | 8 +- .../core/tests/test_serializers/__init__.py | 0 .../test_ai_provider_serializer.py | 332 +++++++++++ .../test_api_key_serializer.py | 266 +++++++++ .../test_app_ai_provider_serializer.py | 334 +++++++++++ .../test_app_integration_serializer.py | 299 ++++++++++ .../test_app_model_serializer.py | 331 +++++++++++ ...est_app_notification_profile_serializer.py | 126 +++++ .../test_application_serializer.py | 142 +++++ .../test_change_password_serializer.py | 208 +++++++ .../test_chatroom_serializer.py | 289 ++++++++++ .../test_configure_app_serializer.py | 185 ++++++ .../test_integration_serializer.py | 245 ++++++++ .../test_knowledge_base_serializer.py | 366 ++++++++++++ .../test_llm_model_serializer.py | 164 ++++++ .../test_message_serializer.py | 121 ++++ .../test_notification_profiles_serializer.py | 148 +++++ .../test_password_serializer.py | 78 +++ .../test_prompt_config_serializer.py | 154 +++++ .../test_serializers/test_user_serializer.py | 145 +++++ .../test_version_control_serializers.py | 528 ++++++++++++++++++ 22 files changed, 4469 insertions(+), 13 deletions(-) create mode 100644 backend/core/tests/test_serializers/__init__.py create mode 100644 backend/core/tests/test_serializers/test_ai_provider_serializer.py create mode 100644 backend/core/tests/test_serializers/test_api_key_serializer.py create mode 100644 backend/core/tests/test_serializers/test_app_ai_provider_serializer.py create mode 100644 backend/core/tests/test_serializers/test_app_integration_serializer.py create mode 100644 backend/core/tests/test_serializers/test_app_model_serializer.py create mode 100644 backend/core/tests/test_serializers/test_app_notification_profile_serializer.py create mode 100644 backend/core/tests/test_serializers/test_application_serializer.py create mode 100644 backend/core/tests/test_serializers/test_change_password_serializer.py create mode 100644 backend/core/tests/test_serializers/test_chatroom_serializer.py create mode 100644 backend/core/tests/test_serializers/test_configure_app_serializer.py create mode 100644 backend/core/tests/test_serializers/test_integration_serializer.py create mode 100644 backend/core/tests/test_serializers/test_knowledge_base_serializer.py create mode 100644 backend/core/tests/test_serializers/test_llm_model_serializer.py create mode 100644 backend/core/tests/test_serializers/test_message_serializer.py create mode 100644 backend/core/tests/test_serializers/test_notification_profiles_serializer.py create mode 100644 backend/core/tests/test_serializers/test_password_serializer.py create mode 100644 backend/core/tests/test_serializers/test_prompt_config_serializer.py create mode 100644 backend/core/tests/test_serializers/test_user_serializer.py create mode 100644 backend/core/tests/test_serializers/test_version_control_serializers.py diff --git a/backend/core/serializers/configure_app.py b/backend/core/serializers/configure_app.py index bb897a2..3510833 100644 --- a/backend/core/serializers/configure_app.py +++ b/backend/core/serializers/configure_app.py @@ -50,22 +50,17 @@ def __init__(self, *args, **kwargs): request = self.context.get('request') user = getattr(request, 'user', None) if user: - self.fields['integration'].queryset = Integration.objects.filter(owner=user) + self.fields['integration'].queryset = Integration.objects.filter(creator=user) def validate(self, attrs): integration = attrs['integration'] app = self.context.get("application") branch_name = attrs.get("branch_name") - if integration.owner != app.owner: + if integration.creator != app.owner: raise serializers.ValidationError("Integration and application owner mismatch.") - if integration.type != attrs["type"]: - raise serializers.ValidationError( - f"Integration type '{integration.type}' does not match '{attrs['type']}'" - ) - - if integration.type == "pms" and integration.provider.lower() == "github": + if attrs["type"] == "pms" and integration.provider.lower() == "github": if not branch_name: raise serializers.ValidationError("Branch is required for GitHub PMS integrations.") @@ -73,6 +68,6 @@ def validate(self, attrs): def validate_integration(self, value): app = self.context.get('application') - if value.owner != app.owner: + if value.creator != app.owner: raise serializers.ValidationError("Integration and application owner mismatch.") return value \ No newline at end of file diff --git a/backend/core/serializers/version_control_serializers.py b/backend/core/serializers/version_control_serializers.py index 6baedcf..93b867d 100644 --- a/backend/core/serializers/version_control_serializers.py +++ b/backend/core/serializers/version_control_serializers.py @@ -21,7 +21,7 @@ class Meta: model = VCIssueComment fields = [ 'id', 'uuid', 'external_id', 'body', 'author', 'author_association', - 'created_at_field', 'updated_at_field', 'url', 'created_at', 'updated_at' + 'url', 'created_at', 'updated_at' ] read_only_fields = ['id', 'uuid', 'created_at', 'updated_at'] @@ -35,7 +35,7 @@ class Meta: fields = [ 'id', 'uuid', 'external_id', 'number', 'title', 'body', 'state', 'author', 'author_association', 'assignees', 'labels', 'milestone', - 'locked', 'created_at_field', 'updated_at_field', 'closed_at', 'url', + 'locked', 'closed_at', 'url', 'comments', 'comment_count', 'created_at', 'updated_at' ] read_only_fields = ['id', 'uuid', 'created_at', 'updated_at'] @@ -61,7 +61,7 @@ class Meta: model = VCPRComment fields = [ 'id', 'uuid', 'external_id', 'body', 'author', 'author_association', - 'created_at_field', 'updated_at_field', 'url', 'created_at', 'updated_at' + 'url', 'created_at', 'updated_at' ] read_only_fields = ['id', 'uuid', 'created_at', 'updated_at'] @@ -79,7 +79,7 @@ class Meta: 'author', 'author_association', 'assignees', 'reviewers', 'labels', 'milestone', 'head_branch', 'base_branch', 'merged', 'merged_at', 'merge_commit_sha', 'additions', 'deletions', 'changed_files', - 'created_at_field', 'updated_at_field', 'closed_at', 'url', + 'closed_at', 'url', 'comments', 'files', 'comment_count', 'file_count', 'created_at', 'updated_at' ] diff --git a/backend/core/tests/test_serializers/__init__.py b/backend/core/tests/test_serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/core/tests/test_serializers/test_ai_provider_serializer.py b/backend/core/tests/test_serializers/test_ai_provider_serializer.py new file mode 100644 index 0000000..914a882 --- /dev/null +++ b/backend/core/tests/test_serializers/test_ai_provider_serializer.py @@ -0,0 +1,332 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.models import AIProvider +from core.serializers.ai_provider import ( + AIProviderSerializer, + AIProviderCreateSerializer, +) +from core.tests.factories import UserFactory, AIProviderFactory + + +@pytest.mark.unit +class TestAIProviderSerializer: + + def test_serialization_excludes_provider_api_key(self): + user = UserFactory() + provider = AIProviderFactory( + creator=user, + name="Test Provider", + provider="gemini", + provider_api_key="secret-key-123" + ) + + serializer = AIProviderSerializer(provider) + data = serializer.data + + assert 'provider_api_key' not in data + assert data['name'] == "Test Provider" + assert data['provider'] == "gemini" + + def test_serialization_includes_expected_fields(self): + user = UserFactory() + provider = AIProviderFactory( + creator=user, + name="Test Provider", + provider="gemini" + ) + + serializer = AIProviderSerializer(provider) + data = serializer.data + + expected_fields = ['id', 'uuid', 'name', 'provider', 'is_builtin', 'creator', 'created_at', 'updated_at', 'metadata'] + for field in expected_fields: + assert field in data + + +@pytest.mark.unit +class TestAIProviderCreateSerializerInit: + def test_init_without_instance(self): + user = UserFactory() + data = { + 'name': 'Test Provider', + 'provider': 'gemini', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + + assert serializer.fields['provider_api_key'].required + assert not serializer.fields['provider_api_key'].allow_blank + assert not serializer.fields['provider'].read_only + + def test_init_with_instance(self): + user = UserFactory() + provider = AIProviderFactory(creator=user, provider="gemini") + + serializer = AIProviderCreateSerializer( + instance=provider, + data={'name': 'Updated Name'}, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + + assert not serializer.fields['provider_api_key'].required + assert serializer.fields['provider_api_key'].allow_blank + assert serializer.fields['provider'].read_only + + +@pytest.mark.unit +class TestAIProviderCreateSerializerValidateProvider: + def test_validate_provider_valid(self): + serializer = AIProviderCreateSerializer() + result = serializer.validate_provider('gemini') + assert result == 'gemini' + + def test_validate_provider_invalid(self): + serializer = AIProviderCreateSerializer() + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_provider('invalid_provider') + + assert "not supported" in str(exc_info.value) + assert "Google Gemini" in str(exc_info.value) + + +@pytest.mark.unit +class TestAIProviderCreateSerializerValidate: + def test_validate_gemini_provider_with_base_url(self): + user = UserFactory() + data = { + 'name': 'Gemini Provider', + 'provider': 'gemini', + 'base_url': 'https://api.example.com', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + assert serializer.is_valid() + + def test_validate_gemini_provider_without_base_url_on_create(self): + user = UserFactory() + data = { + 'name': 'Gemini Provider', + 'provider': 'gemini', + 'base_url': '', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + assert serializer.is_valid() + + def test_validate_gemini_provider_with_none_base_url_on_create(self): + user = UserFactory() + data = { + 'name': 'Gemini Provider', + 'provider': 'gemini', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + assert serializer.is_valid() + + def test_validate_gemini_provider_with_whitespace_base_url_on_create(self): + user = UserFactory() + data = { + 'name': 'Gemini Provider', + 'provider': 'gemini', + 'base_url': ' ', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + assert serializer.is_valid() + + def test_validate_gemini_provider_without_base_url_on_update(self): + user = UserFactory() + provider = AIProviderFactory(creator=user, provider='gemini') + + data = {'base_url': ''} + serializer = AIProviderCreateSerializer( + instance=provider, + data=data, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + assert serializer.is_valid() + + def test_validate_non_custom_provider_without_base_url(self): + user = UserFactory() + data = { + 'name': 'Gemini Provider', + 'provider': 'gemini', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + assert serializer.is_valid() + + def test_validate_gemini_provider_with_base_url_on_update(self): + user = UserFactory() + provider = AIProviderFactory(creator=user, provider='gemini') + + data = {'base_url': 'https://api.example.com/v2'} + serializer = AIProviderCreateSerializer( + instance=provider, + data=data, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + assert serializer.is_valid() + + +@pytest.mark.unit +class TestAIProviderCreateSerializerCreate: + def test_create_sets_creator_from_request(self): + user = UserFactory() + data = { + 'name': 'Test Provider', + 'provider': 'gemini', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + assert serializer.is_valid() + provider = serializer.save() + + assert provider.creator == user + + def test_create_extracts_metadata(self): + user = UserFactory() + data = { + 'name': 'Test Provider', + 'provider': 'gemini', + 'base_url': 'https://api.example.com', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + assert serializer.is_valid() + provider = serializer.save() + + assert 'base_url' in provider.metadata + assert provider.metadata['base_url'] == 'https://api.example.com' + + def test_create_without_base_url(self): + user = UserFactory() + data = { + 'name': 'Test Provider', + 'provider': 'gemini', + 'provider_api_key': 'test-key-123' + } + + serializer = AIProviderCreateSerializer(data=data, context={'request': type('MockRequest', (), {'user': user})()}) + assert serializer.is_valid() + provider = serializer.save() + + assert provider.name == 'Test Provider' + assert provider.provider == 'gemini' + + +@pytest.mark.unit +class TestAIProviderCreateSerializerUpdate: + def test_update_with_new_api_key(self): + user = UserFactory() + provider = AIProviderFactory(creator=user, provider_api_key='old-key-123') + + data = {'provider_api_key': 'new-key-456'} + serializer = AIProviderCreateSerializer( + instance=provider, + data=data, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + assert serializer.is_valid() + updated_provider = serializer.save() + + assert updated_provider.provider_api_key == 'new-key-456' + + def test_update_without_api_key_preserves_existing(self): + user = UserFactory() + provider = AIProviderFactory(creator=user, provider_api_key='existing-key-123') + + data = {'name': 'Updated Name'} + serializer = AIProviderCreateSerializer( + instance=provider, + data=data, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + assert serializer.is_valid() + updated_provider = serializer.save() + + assert updated_provider.provider_api_key == 'existing-key-123' + + def test_update_with_empty_api_key_preserves_existing(self): + user = UserFactory() + provider = AIProviderFactory(creator=user, provider_api_key='existing-key-123') + + data = {'provider_api_key': ''} + serializer = AIProviderCreateSerializer( + instance=provider, + data=data, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + assert serializer.is_valid() + updated_provider = serializer.save() + + assert updated_provider.provider_api_key == 'existing-key-123' + + def test_update_with_whitespace_api_key_preserves_existing(self): + user = UserFactory() + provider = AIProviderFactory(creator=user, provider_api_key='existing-key-123') + + data = {'provider_api_key': ' '} + serializer = AIProviderCreateSerializer( + instance=provider, + data=data, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + assert serializer.is_valid() + updated_provider = serializer.save() + + assert updated_provider.provider_api_key == 'existing-key-123' + + def test_update_merges_metadata(self): + user = UserFactory() + provider = AIProviderFactory( + creator=user, + metadata={'old_field': 'old_value', 'name': 'Old Name'} + ) + + data = {'base_url': 'https://new-api.com'} + serializer = AIProviderCreateSerializer( + instance=provider, + data=data, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + assert serializer.is_valid() + updated_provider = serializer.save() + + assert updated_provider.metadata['base_url'] == 'https://new-api.com' + assert updated_provider.metadata['old_field'] == 'old_value' + assert updated_provider.metadata['name'] == 'Old Name' + + def test_update_name(self): + user = UserFactory() + provider = AIProviderFactory(creator=user, name='Old Name') + + data = {'name': 'New Name'} + serializer = AIProviderCreateSerializer( + instance=provider, + data=data, + partial=True, + context={'request': type('MockRequest', (), {'user': user})()} + ) + assert serializer.is_valid() + updated_provider = serializer.save() + + assert updated_provider.name == 'New Name' diff --git a/backend/core/tests/test_serializers/test_api_key_serializer.py b/backend/core/tests/test_serializers/test_api_key_serializer.py new file mode 100644 index 0000000..c33454c --- /dev/null +++ b/backend/core/tests/test_serializers/test_api_key_serializer.py @@ -0,0 +1,266 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock, patch + +from core.models import ApplicationAPIKey +from core.serializers.api_key import APIKeySerializer +from core.tests.factories import UserFactory, ApplicationFactory + + +@pytest.mark.unit +class TestAPIKeySerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory() + api_key = ApplicationAPIKey( + application=application, + name="Test API Key", + permissions=['read', 'write'], + owner=user + ) + + serializer = APIKeySerializer(api_key) + data = serializer.data + + expected_fields = ['name', 'permissions', 'id', 'created', 'owner'] + for field in expected_fields: + assert field in data + + def test_serialization_excludes_api_key(self): + user = UserFactory() + application = ApplicationFactory() + api_key = ApplicationAPIKey( + application=application, + name="Test API Key", + permissions=['read'], + owner=user + ) + + serializer = APIKeySerializer(api_key) + data = serializer.data + + assert 'api_key' not in data + assert 'hashed_api_key' not in data + + def test_permissions_field_validation_valid_choices(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read', 'write', 'delete'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + + def test_permissions_field_validation_invalid_choice(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read', 'invalid_permission'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert not serializer.is_valid() + assert 'permissions' in serializer.errors + + +@pytest.mark.unit +class TestAPIKeySerializerGenerateApiKey: + def test_generate_api_key_returns_string(self): + serializer = APIKeySerializer() + api_key = serializer.generate_api_key() + assert isinstance(api_key, str) + + def test_generate_api_key_length(self): + serializer = APIKeySerializer() + api_key = serializer.generate_api_key() + assert len(api_key) > 0 + + def test_generate_api_key_uniqueness(self): + serializer = APIKeySerializer() + api_key1 = serializer.generate_api_key() + api_key2 = serializer.generate_api_key() + assert api_key1 != api_key2 + + +@pytest.mark.unit +class TestAPIKeySerializerCreate: + def test_create_sets_owner_from_request(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + api_key_instance, api_key_raw = serializer.save() + + assert api_key_instance.owner == user + + def test_create_sets_application(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + api_key_instance, api_key_raw = serializer.save() + + assert api_key_instance.application == application + + def test_create_sets_name(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + api_key_instance, api_key_raw = serializer.save() + + assert api_key_instance.name == 'Test API Key' + + def test_create_sets_permissions(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read', 'write'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + api_key_instance, api_key_raw = serializer.save() + + assert api_key_instance.permissions == ['read', 'write'] + + def test_create_returns_tuple(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + result = serializer.save() + + assert isinstance(result, tuple) + assert len(result) == 2 + assert isinstance(result[0], ApplicationAPIKey) + assert isinstance(result[1], str) + + def test_create_hashes_api_key(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + api_key_instance, api_key_raw = serializer.save() + + assert api_key_instance.hashed_api_key != api_key_raw + assert api_key_instance.hashed_api_key is not None + assert len(api_key_instance.hashed_api_key) > 0 + + def test_create_invalid_application_uuid(self): + from django.core.exceptions import ValidationError as DjangoValidationError + + user = UserFactory() + + data = { + 'name': 'Test API Key', + 'permissions': ['read'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': 'invalid-uuid'} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + + with pytest.raises(DjangoValidationError): + serializer.save() + + def test_create_with_all_permissions(self): + user = UserFactory() + application = ApplicationFactory() + + data = { + 'name': 'Full Access Key', + 'permissions': ['read', 'write', 'delete'] + } + + mock_request = Mock() + mock_request.user = user + mock_request.resolver_match = Mock() + mock_request.resolver_match.kwargs = {'application_uuid': application.uuid} + + serializer = APIKeySerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + api_key_instance, api_key_raw = serializer.save() + + assert api_key_instance.permissions == ['read', 'write', 'delete'] diff --git a/backend/core/tests/test_serializers/test_app_ai_provider_serializer.py b/backend/core/tests/test_serializers/test_app_ai_provider_serializer.py new file mode 100644 index 0000000..77b831a --- /dev/null +++ b/backend/core/tests/test_serializers/test_app_ai_provider_serializer.py @@ -0,0 +1,334 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.models import AppAIProvider, AIProvider +from core.serializers.app_ai_provider import ( + AppAIProviderSerializer, + AppAIProviderCreateSerializer, + AppAIProviderUpdateSerializer, +) +from core.tests.factories import UserFactory, AIProviderFactory, ApplicationFactory + + +@pytest.mark.unit +class TestAppAIProviderSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user) + app_ai_provider = AppAIProvider( + application=application, + ai_provider=ai_provider, + context='test', + capability='text', + external_model_id='model-123' + ) + + serializer = AppAIProviderSerializer(app_ai_provider) + data = serializer.data + + expected_fields = ['id', 'uuid', 'ai_provider', 'context', 'capability', 'priority', 'external_model_id', 'is_active', 'created_at', 'updated_at'] + for field in expected_fields: + assert field in data + + def test_serialization_includes_nested_ai_provider(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user, name='Test Provider') + app_ai_provider = AppAIProvider( + application=application, + ai_provider=ai_provider, + context='test', + capability='text' + ) + + serializer = AppAIProviderSerializer(app_ai_provider) + data = serializer.data + + assert 'ai_provider' in data + assert data['ai_provider']['name'] == 'Test Provider' + + +@pytest.mark.unit +class TestAppAIProviderCreateSerializer: + def test_validate_ai_provider_id_valid_owned_provider(self): + user = UserFactory() + ai_provider = AIProviderFactory(creator=user) + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderCreateSerializer(context={'request': mock_request}) + result = serializer.validate_ai_provider_id(ai_provider.id) + + assert result == ai_provider.id + + def test_validate_ai_provider_id_valid_builtin_provider(self): + user = UserFactory() + ai_provider = AIProviderFactory(creator=user, is_builtin=True) + + other_user = UserFactory() + mock_request = Mock() + mock_request.user = other_user + + serializer = AppAIProviderCreateSerializer(context={'request': mock_request}) + result = serializer.validate_ai_provider_id(ai_provider.id) + + assert result == ai_provider.id + + def test_validate_ai_provider_id_unowned_provider(self): + user_a = UserFactory() + user_b = UserFactory() + ai_provider = AIProviderFactory(creator=user_a, is_builtin=False) + + mock_request = Mock() + mock_request.user = user_b + + serializer = AppAIProviderCreateSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_ai_provider_id(ai_provider.id) + + assert "don't own this AI provider" in str(exc_info.value) + + def test_validate_ai_provider_id_not_found(self): + user = UserFactory() + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderCreateSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_ai_provider_id(99999) + + assert "not found" in str(exc_info.value) + + def test_create_new_config(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user) + + data = { + 'ai_provider_id': ai_provider.id, + 'context': 'test', + 'capability': 'text', + 'external_model_id': 'model-123' + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_ai_provider = serializer.save() + + assert app_ai_provider.application == application + assert app_ai_provider.ai_provider == ai_provider + assert app_ai_provider.context == 'test' + assert app_ai_provider.capability == 'text' + assert app_ai_provider.external_model_id == 'model-123' + + def test_create_updates_existing_config(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider_old = AIProviderFactory(creator=user) + ai_provider_new = AIProviderFactory(creator=user) + + existing_config = AppAIProvider.objects.create( + application=application, + ai_provider=ai_provider_old, + context='test', + capability='text', + external_model_id='old-model' + ) + + data = { + 'ai_provider_id': ai_provider_new.id, + 'context': 'test', + 'capability': 'text', + 'external_model_id': 'new-model' + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_ai_provider = serializer.save() + + assert app_ai_provider.id == existing_config.id + assert app_ai_provider.ai_provider == ai_provider_new + assert app_ai_provider.external_model_id == 'new-model' + + assert AppAIProvider.objects.filter(application=application, context='test', capability='text').count() == 1 + + def test_create_deletes_old_duplicates(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user) + + config1 = AppAIProvider.objects.create( + application=application, + ai_provider=ai_provider, + context='test', + capability='text', + external_model_id='model-1' + ) + config2 = AppAIProvider.objects.create( + application=application, + ai_provider=ai_provider, + context='test', + capability='text', + external_model_id='model-2' + ) + + data = { + 'ai_provider_id': ai_provider.id, + 'context': 'test', + 'capability': 'text', + 'external_model_id': 'model-3' + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_ai_provider = serializer.save() + + assert AppAIProvider.objects.filter(application=application, context='test', capability='text').count() == 1 + + def test_create_without_external_model_id(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user) + + data = { + 'ai_provider_id': ai_provider.id, + 'context': 'test', + 'capability': 'text' + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_ai_provider = serializer.save() + + assert app_ai_provider.external_model_id is None + + def test_to_representation_uses_app_ai_provider_serializer(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user) + app_ai_provider = AppAIProvider.objects.create( + application=application, + ai_provider=ai_provider, + context='test', + capability='text' + ) + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderCreateSerializer( + context={'request': mock_request, 'application': application} + ) + data = serializer.to_representation(app_ai_provider) + + assert 'ai_provider' in data + assert 'context' in data + assert 'capability' in data + + +@pytest.mark.unit +class TestAppAIProviderUpdateSerializer: + def test_update_external_model_id(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user) + app_ai_provider = AppAIProvider.objects.create( + application=application, + ai_provider=ai_provider, + context='test', + capability='text', + external_model_id='old-model' + ) + + data = {'external_model_id': 'new-model'} + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderUpdateSerializer( + instance=app_ai_provider, + data=data, + partial=True, + context={'request': mock_request} + ) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.external_model_id == 'new-model' + + def test_update_without_external_model_id(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user) + app_ai_provider = AppAIProvider.objects.create( + application=application, + ai_provider=ai_provider, + context='test', + capability='text', + external_model_id='old-model' + ) + + data = {} + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderUpdateSerializer( + instance=app_ai_provider, + data=data, + partial=True, + context={'request': mock_request} + ) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.external_model_id == 'old-model' + + def test_to_representation_uses_app_ai_provider_serializer(self): + user = UserFactory() + application = ApplicationFactory() + ai_provider = AIProviderFactory(creator=user) + app_ai_provider = AppAIProvider.objects.create( + application=application, + ai_provider=ai_provider, + context='test', + capability='text' + ) + + mock_request = Mock() + mock_request.user = user + + serializer = AppAIProviderUpdateSerializer(context={'request': mock_request}) + data = serializer.to_representation(app_ai_provider) + + assert 'ai_provider' in data + assert 'context' in data + assert 'capability' in data diff --git a/backend/core/tests/test_serializers/test_app_integration_serializer.py b/backend/core/tests/test_serializers/test_app_integration_serializer.py new file mode 100644 index 0000000..25c93ee --- /dev/null +++ b/backend/core/tests/test_serializers/test_app_integration_serializer.py @@ -0,0 +1,299 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.models import AppIntegration, Integration +from core.serializers.app_integration import ( + AppIntegrationSerializer, + AppIntegrationCreateSerializer, + AppIntegrationViewSerializer, +) +from core.tests.factories import UserFactory, IntegrationFactory, ApplicationFactory + + +@pytest.mark.unit +class TestAppIntegrationSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user) + app_integration = AppIntegration( + application=application, + integration=integration, + integration_type='version_control', + metadata={'key': 'value'}, + is_active=True + ) + + serializer = AppIntegrationSerializer(app_integration) + data = serializer.data + + expected_fields = ['uuid', 'integration', 'integration_type', 'metadata', 'is_active', 'created_at', 'updated_at'] + for field in expected_fields: + assert field in data + + def test_serialization_includes_nested_integration(self): + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user, provider='github') + app_integration = AppIntegration( + application=application, + integration=integration, + integration_type='version_control' + ) + + serializer = AppIntegrationSerializer(app_integration) + data = serializer.data + + assert 'integration' in data + assert data['integration']['provider'] == 'github' + + def test_app_integration_view_serializer_is_alias(self): + assert AppIntegrationViewSerializer == AppIntegrationSerializer + + +@pytest.mark.unit +class TestAppIntegrationCreateSerializer: + def test_validate_integration_uuid_valid_owned(self): + user = UserFactory() + integration = IntegrationFactory(creator=user) + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer(context={'request': mock_request}) + result = serializer.validate_integration_uuid(integration.uuid) + + assert result == integration.uuid + assert serializer._integration == integration + + def test_validate_integration_uuid_not_found(self): + user = UserFactory() + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_integration_uuid('00000000-0000-0000-0000-000000000000') + + assert "not found" in str(exc_info.value) + + def test_validate_integration_uuid_unowned(self): + user_a = UserFactory() + user_b = UserFactory() + integration = IntegrationFactory(creator=user_a) + + mock_request = Mock() + mock_request.user = user_b + + serializer = AppIntegrationCreateSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_integration_uuid(integration.uuid) + + assert "don't own this integration" in str(exc_info.value) + + def test_validate_integration_type_supported(self): + user = UserFactory() + integration = IntegrationFactory(creator=user, provider='github') + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer(context={'request': mock_request}) + serializer._integration = integration + + data = {'integration_type': 'version_control'} + result = serializer.validate(data) + + assert result == data + + def test_validate_integration_type_unsupported(self): + user = UserFactory() + integration = IntegrationFactory(creator=user, provider='github') + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer(context={'request': mock_request}) + serializer._integration = integration + + data = {'integration_type': 'unsupported_type'} + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate(data) + + assert "not supported" in str(exc_info.value) + + def test_create_new_integration(self): + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user, provider='github') + + data = { + 'integration_uuid': integration.uuid, + 'integration_type': 'version_control', + 'metadata': {'key': 'value'} + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_integration = serializer.save() + + assert app_integration.application == application + assert app_integration.integration == integration + assert app_integration.integration_type == 'version_control' + assert app_integration.metadata == {'key': 'value'} + assert app_integration.is_active is True + + def test_create_updates_existing_integration(self): + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user, provider='github') + + existing = AppIntegration.objects.create( + application=application, + integration=integration, + integration_type='version_control', + is_active=False + ) + + data = { + 'integration_uuid': integration.uuid, + 'integration_type': 'version_control', + 'metadata': {'new_key': 'new_value'} + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_integration = serializer.save() + + assert app_integration.id == existing.id + assert app_integration.metadata == {'new_key': 'new_value'} + assert app_integration.is_active is True + + def test_create_with_tools(self): + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user, provider='github') + + data = { + 'integration_uuid': integration.uuid, + 'integration_type': 'version_control', + 'tools': { + 'github_version_control:list_commits': {'is_enabled': True}, + 'github_version_control:list_pull_requests': {'is_enabled': False} + } + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_integration = serializer.save() + + assert app_integration.tool_configs.filter(tool_id='github_version_control:list_commits', is_enabled=True).exists() + assert app_integration.tool_configs.filter(tool_id='github_version_control:list_pull_requests', is_enabled=False).exists() + + def test_create_with_custom_tools(self): + import uuid as uuid_lib + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user, provider='github') + + data = { + 'integration_uuid': integration.uuid, + 'integration_type': 'version_control', + 'custom_tools': [ + {'uuid': str(uuid_lib.uuid4()), 'is_enabled': True}, + {'uuid': str(uuid_lib.uuid4()), 'is_enabled': False} + ] + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_integration = serializer.save() + + def test_create_without_metadata(self): + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user, provider='github') + + data = { + 'integration_uuid': integration.uuid, + 'integration_type': 'version_control' + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_integration = serializer.save() + + assert app_integration.metadata is None + + def test_create_without_tools(self): + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user, provider='github') + + data = { + 'integration_uuid': integration.uuid, + 'integration_type': 'version_control' + } + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + app_integration = serializer.save() + + assert app_integration.tool_configs.count() == 0 + + def test_to_representation_uses_app_integration_serializer(self): + user = UserFactory() + application = ApplicationFactory() + integration = IntegrationFactory(creator=user) + app_integration = AppIntegration.objects.create( + application=application, + integration=integration, + integration_type='version_control' + ) + + mock_request = Mock() + mock_request.user = user + + serializer = AppIntegrationCreateSerializer(context={'request': mock_request, 'application': application}) + data = serializer.to_representation(app_integration) + + assert 'integration' in data + assert 'integration_type' in data diff --git a/backend/core/tests/test_serializers/test_app_model_serializer.py b/backend/core/tests/test_serializers/test_app_model_serializer.py new file mode 100644 index 0000000..afbfe32 --- /dev/null +++ b/backend/core/tests/test_serializers/test_app_model_serializer.py @@ -0,0 +1,331 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.models import AppModel, LLMModel +from core.serializers.app_model import ( + AppModelViewSerializer, + ConfigureAppModelSerializer, + ConfigureAppModelsSerializer, +) +from core.tests.factories import UserFactory, ApplicationFactory + + +@pytest.mark.unit +class TestAppModelViewSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory() + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + app_model = AppModel( + application=application, + llm_model=llm_model + ) + + serializer = AppModelViewSerializer(app_model) + data = serializer.data + + assert data['name'] == 'Test Model' + assert data['model_name'] == 'test-model' + + def test_serialization_includes_nested_llm_model(self): + user = UserFactory() + application = ApplicationFactory() + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + app_model = AppModel( + application=application, + llm_model=llm_model + ) + + serializer = AppModelViewSerializer(app_model) + data = serializer.data + + assert data['name'] == 'Test Model' + assert data['model_type'] == 'text' + + def test_to_representation_returns_llm_model_data(self): + user = UserFactory() + application = ApplicationFactory() + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + app_model = AppModel( + application=application, + llm_model=llm_model + ) + + serializer = AppModelViewSerializer(app_model) + data = serializer.to_representation(app_model) + + assert data['name'] == 'Test Model' + assert 'application' not in data + + +@pytest.mark.unit +class TestConfigureAppModelSerializer: + def test_get_fields_filters_queryset_for_user(self): + user = UserFactory() + user_model = LLMModel.objects.create( + owner=user, + name='User Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='user-model', + model_type='text', + is_default=False + ) + other_user = UserFactory() + other_model = LLMModel.objects.create( + owner=other_user, + name='Other Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='other-model', + model_type='text', + is_default=False + ) + default_model = LLMModel.objects.create( + owner=user, + name='Default Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='default-model', + model_type='text', + is_default=True + ) + + mock_request = Mock() + mock_request.user = user + + serializer = ConfigureAppModelSerializer(context={'request': mock_request}) + fields = serializer.get_fields() + + assert user_model in fields['llm_model'].queryset + assert default_model in fields['llm_model'].queryset + assert other_model not in fields['llm_model'].queryset + + def test_get_fields_returns_empty_queryset_for_no_user(self): + mock_request = Mock() + mock_request.user = None + + serializer = ConfigureAppModelSerializer(context={'request': mock_request}) + fields = serializer.get_fields() + + assert fields['llm_model'].queryset.count() == 0 + + def test_validate_model_type_matches(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + + attrs = { + 'model_type': 'text', + 'llm_model': llm_model + } + + serializer = ConfigureAppModelSerializer(context={'application': application}) + result = serializer.validate(attrs) + + assert result == attrs + + def test_validate_model_type_mismatch(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + + attrs = { + 'model_type': 'embedding', + 'llm_model': llm_model + } + + serializer = ConfigureAppModelSerializer(context={'application': application}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate(attrs) + + assert "does not match" in str(exc_info.value) + + def test_validate_llm_model_owner_matches_application_owner(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text', + is_default=False + ) + + serializer = ConfigureAppModelSerializer(context={'application': application}) + result = serializer.validate_llm_model(llm_model) + + assert result == llm_model + + def test_validate_llm_model_default_model_allowed(self): + user_a = UserFactory() + user_b = UserFactory() + application = ApplicationFactory(owner=user_a) + llm_model = LLMModel.objects.create( + owner=user_b, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text', + is_default=True + ) + + serializer = ConfigureAppModelSerializer(context={'application': application}) + result = serializer.validate_llm_model(llm_model) + + assert result == llm_model + + def test_validate_llm_model_owner_mismatch(self): + user_a = UserFactory() + user_b = UserFactory() + application = ApplicationFactory(owner=user_a) + llm_model = LLMModel.objects.create( + owner=user_b, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text', + is_default=False + ) + + serializer = ConfigureAppModelSerializer(context={'application': application}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_llm_model(llm_model) + + assert "owner mismatch" in str(exc_info.value) + + +@pytest.mark.unit +class TestConfigureAppModelsSerializer: + def test_serializer_accepts_list_of_models(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + + data = { + 'models': [ + { + 'model_type': 'text', + 'llm_model': str(llm_model.uuid) + } + ] + } + + mock_request = Mock() + mock_request.user = user + + serializer = ConfigureAppModelsSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + + def test_serializer_accepts_empty_list(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + data = {'models': []} + + mock_request = Mock() + mock_request.user = user + + serializer = ConfigureAppModelsSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert serializer.is_valid() + + def test_serializer_validates_each_model(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + + data = { + 'models': [ + { + 'model_type': 'text', + 'llm_model': str(llm_model.uuid) + }, + { + 'model_type': 'embedding', + 'llm_model': str(llm_model.uuid) + } + ] + } + + mock_request = Mock() + mock_request.user = user + + serializer = ConfigureAppModelsSerializer( + data=data, + context={'request': mock_request, 'application': application} + ) + assert not serializer.is_valid() + assert 'models' in serializer.errors diff --git a/backend/core/tests/test_serializers/test_app_notification_profile_serializer.py b/backend/core/tests/test_serializers/test_app_notification_profile_serializer.py new file mode 100644 index 0000000..63d84f0 --- /dev/null +++ b/backend/core/tests/test_serializers/test_app_notification_profile_serializer.py @@ -0,0 +1,126 @@ +import pytest +from rest_framework import serializers + +from core.models import AppNotificationProfile, NotificationProfile +from core.serializers.app_notification_profile import AppNotificationProfileSerializer +from core.tests.factories import UserFactory, ApplicationFactory, NotificationProfileFactory + + +@pytest.mark.unit +class TestAppNotificationProfileSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + notification_profile = NotificationProfileFactory(owner=user) + app_notification_profile = AppNotificationProfile( + application=application, + notification_profile=notification_profile + ) + + serializer = AppNotificationProfileSerializer(app_notification_profile) + data = serializer.data + + expected_fields = ['id', 'application', 'notification_profile', 'created_at'] + for field in expected_fields: + assert field in data + + def test_serialization_includes_nested_notification_profile(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + notification_profile = NotificationProfileFactory(owner=user, type='email') + app_notification_profile = AppNotificationProfile( + application=application, + notification_profile=notification_profile + ) + + serializer = AppNotificationProfileSerializer(app_notification_profile) + data = serializer.data + + assert 'notification_profile' in data + assert data['notification_profile']['type'] == 'email' + + def test_serialization_notification_profile_id_is_write_only(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + notification_profile = NotificationProfileFactory(owner=user) + app_notification_profile = AppNotificationProfile( + application=application, + notification_profile=notification_profile + ) + + serializer = AppNotificationProfileSerializer(app_notification_profile) + data = serializer.data + + assert 'notification_profile_id' not in data + + def test_create_with_notification_profile_id(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + notification_profile = NotificationProfileFactory(owner=user) + + data = { + 'application': application.id, + 'notification_profile_id': notification_profile.id + } + + serializer = AppNotificationProfileSerializer(data=data) + assert serializer.is_valid() + app_notification_profile = serializer.save() + + assert app_notification_profile.application == application + assert app_notification_profile.notification_profile == notification_profile + + def test_create_without_notification_profile_id(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + data = { + 'application': application.id + } + + serializer = AppNotificationProfileSerializer(data=data) + assert not serializer.is_valid() + assert 'notification_profile_id' in serializer.errors + + def test_update_with_notification_profile_id(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + notification_profile_old = NotificationProfileFactory(owner=user) + notification_profile_new = NotificationProfileFactory(owner=user) + app_notification_profile = AppNotificationProfile.objects.create( + application=application, + notification_profile=notification_profile_old + ) + + data = { + 'notification_profile_id': notification_profile_new.id + } + + serializer = AppNotificationProfileSerializer( + instance=app_notification_profile, + data=data, + partial=True + ) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.notification_profile == notification_profile_new + + def test_unique_together_constraint(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + notification_profile = NotificationProfileFactory(owner=user) + + AppNotificationProfile.objects.create( + application=application, + notification_profile=notification_profile + ) + + data = { + 'application': application.id, + 'notification_profile_id': notification_profile.id + } + + serializer = AppNotificationProfileSerializer(data=data) + assert not serializer.is_valid() + assert 'non_field_errors' in serializer.errors diff --git a/backend/core/tests/test_serializers/test_application_serializer.py b/backend/core/tests/test_serializers/test_application_serializer.py new file mode 100644 index 0000000..ecab030 --- /dev/null +++ b/backend/core/tests/test_serializers/test_application_serializer.py @@ -0,0 +1,142 @@ +import pytest +from rest_framework import serializers + +from core.models import Application, LLMModel +from core.serializers.application import ( + ApplicationCreateSerializer, + ApplicationViewSerializer, +) +from core.tests.factories import UserFactory, ApplicationFactory + + +@pytest.mark.unit +class TestApplicationCreateSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + serializer = ApplicationCreateSerializer(application) + data = serializer.data + + expected_fields = ['name'] + for field in expected_fields: + assert field in data + + def test_create_with_name(self): + user = UserFactory() + data = {'name': 'Test Application'} + + serializer = ApplicationCreateSerializer(data=data) + assert serializer.is_valid() + application = serializer.save(owner=user) + + assert application.name == 'Test Application' + assert application.owner == user + + def test_create_without_name(self): + data = {} + + serializer = ApplicationCreateSerializer(data=data) + assert not serializer.is_valid() + assert 'name' in serializer.errors + + +@pytest.mark.unit +class TestApplicationViewSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + serializer = ApplicationViewSerializer(application) + data = serializer.data + + expected_fields = ['id', 'uuid', 'name', 'owner_id', 'owner', 'llm_models', 'app_integrations'] + for field in expected_fields: + assert field in data + + def test_serialization_includes_nested_owner(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + serializer = ApplicationViewSerializer(application) + data = serializer.data + + assert 'owner' in data + assert data['owner']['id'] == user.id + + def test_serialization_includes_owner_id(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + serializer = ApplicationViewSerializer(application) + data = serializer.data + + assert 'owner_id' in data + assert data['owner_id'] == user.id + + def test_get_llm_models_returns_empty_list(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + serializer = ApplicationViewSerializer(application) + llm_models = serializer.get_llm_models(application) + + assert llm_models == [] + + def test_get_llm_models_returns_configured_models(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + + from core.models.app_model import AppModel + AppModel.objects.create( + application=application, + llm_model=llm_model + ) + + serializer = ApplicationViewSerializer(application) + llm_models = serializer.get_llm_models(application) + + assert len(llm_models) == 1 + assert llm_models[0]['id'] == llm_model.id + assert llm_models[0]['uuid'] == llm_model.uuid + assert llm_models[0]['name'] == 'Test Model' + assert llm_models[0]['model_name'] == 'test-model' + assert llm_models[0]['model_type'] == 'text' + assert llm_models[0]['is_default'] == llm_model.is_default + + def test_get_llm_models_returns_distinct_models(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + api_key='test-key', + api_key_preview='test***', + base_url='https://example.com', + model_name='test-model', + model_type='text' + ) + + from core.models.app_model import AppModel + AppModel.objects.create( + application=application, + llm_model=llm_model + ) + AppModel.objects.create( + application=application, + llm_model=llm_model + ) + + serializer = ApplicationViewSerializer(application) + llm_models = serializer.get_llm_models(application) + + assert len(llm_models) == 1 diff --git a/backend/core/tests/test_serializers/test_change_password_serializer.py b/backend/core/tests/test_serializers/test_change_password_serializer.py new file mode 100644 index 0000000..d27b3f4 --- /dev/null +++ b/backend/core/tests/test_serializers/test_change_password_serializer.py @@ -0,0 +1,208 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.serializers.change_password import ChangePasswordSerializer +from core.tests.factories import UserFactory + + +@pytest.mark.unit +class TestChangePasswordSerializer: + def test_serializer_includes_expected_fields(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + expected_fields = ['current_password', 'new_password', 'confirm_password'] + for field in expected_fields: + assert field in serializer.fields + + def test_fields_are_write_only(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + assert serializer.fields['current_password'].write_only + assert serializer.fields['new_password'].write_only + assert serializer.fields['confirm_password'].write_only + + def test_validate_current_password_correct(self): + user = UserFactory() + user.set_password('oldpassword123') + user.save() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + result = serializer.validate_current_password('oldpassword123') + + assert result == 'oldpassword123' + + def test_validate_current_password_incorrect(self): + user = UserFactory() + user.set_password('oldpassword123') + user.save() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_current_password('wrongpassword') + + assert "incorrect" in str(exc_info.value).lower() + + def test_validate_new_password_too_short(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_new_password('Short1!') + + assert "8 characters" in str(exc_info.value) + + def test_validate_new_password_missing_uppercase(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_new_password('lowercase123!') + + assert "uppercase" in str(exc_info.value).lower() + + def test_validate_new_password_missing_lowercase(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_new_password('UPPERCASE123!') + + assert "lowercase" in str(exc_info.value).lower() + + def test_validate_new_password_missing_number(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_new_password('NoNumber!') + + assert "number" in str(exc_info.value).lower() + + def test_validate_new_password_missing_special_character(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_new_password('NoSpecial123') + + assert "special" in str(exc_info.value).lower() + + def test_validate_new_password_valid(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + result = serializer.validate_new_password('ValidPass123!') + + assert result == 'ValidPass123!' + + def test_validate_passwords_match(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + attrs = { + 'current_password': 'OldPass123!', + 'new_password': 'NewPass123!', + 'confirm_password': 'NewPass123!' + } + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + result = serializer.validate(attrs) + + assert result == attrs + + def test_validate_passwords_dont_match(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + attrs = { + 'current_password': 'OldPass123!', + 'new_password': 'NewPass123!', + 'confirm_password': 'DifferentPass123!' + } + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate(attrs) + + assert "don't match" in str(exc_info.value) + + def test_validate_new_password_same_as_current(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + attrs = { + 'current_password': 'SamePass123!', + 'new_password': 'SamePass123!', + 'confirm_password': 'SamePass123!' + } + + serializer = ChangePasswordSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate(attrs) + + assert "different" in str(exc_info.value).lower() + + def test_save_sets_new_password(self): + user = UserFactory() + user.set_password('OldPass123!') + user.save() + mock_request = Mock() + mock_request.user = user + + data = { + 'current_password': 'OldPass123!', + 'new_password': 'NewPass123!', + 'confirm_password': 'NewPass123!' + } + + serializer = ChangePasswordSerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + updated_user = serializer.save() + + assert updated_user.check_password('NewPass123!') + assert not updated_user.check_password('OldPass123!') + + def test_full_validation_flow(self): + user = UserFactory() + user.set_password('OldPass123!') + user.save() + mock_request = Mock() + mock_request.user = user + + data = { + 'current_password': 'OldPass123!', + 'new_password': 'NewPass456!', + 'confirm_password': 'NewPass456!' + } + + serializer = ChangePasswordSerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + updated_user = serializer.save() + + assert updated_user.check_password('NewPass456!') diff --git a/backend/core/tests/test_serializers/test_chatroom_serializer.py b/backend/core/tests/test_serializers/test_chatroom_serializer.py new file mode 100644 index 0000000..47f6fbc --- /dev/null +++ b/backend/core/tests/test_serializers/test_chatroom_serializer.py @@ -0,0 +1,289 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.models import ChatRoom, ChatroomParticipant +from core.serializers.chatroom import ( + ChatRoomViewSerializer, + ChatRoomNameUpdateSerializer, + ChatRoomWithMessagesSerializer, + ChatroomParticipantSerializer, + ChatRoomPreviewSerializer, + ChatRoomDetailSerializer, +) +from core.tests.factories import UserFactory, ApplicationFactory, AIProviderFactory + + +@pytest.mark.unit +class TestChatRoomViewSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + ai_provider = AIProviderFactory(creator=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application, + ai_provider=ai_provider, + model='gpt-4' + ) + + serializer = ChatRoomViewSerializer(chatroom) + data = serializer.data + + expected_fields = ['uuid', 'name', 'ai_provider', 'model'] + for field in expected_fields: + assert field in data + + def test_serialization_includes_nested_ai_provider(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + ai_provider = AIProviderFactory(creator=user, name='Test Provider') + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application, + ai_provider=ai_provider, + model='gpt-4' + ) + + serializer = ChatRoomViewSerializer(chatroom) + data = serializer.data + + assert 'ai_provider' in data + + +@pytest.mark.unit +class TestChatRoomNameUpdateSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + + serializer = ChatRoomNameUpdateSerializer(chatroom) + data = serializer.data + + expected_fields = ['name'] + for field in expected_fields: + assert field in data + + def test_update_name(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Old Name', + application=application + ) + + data = {'name': 'New Name'} + serializer = ChatRoomNameUpdateSerializer( + instance=chatroom, + data=data, + partial=True + ) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.name == 'New Name' + + +@pytest.mark.unit +class TestChatroomParticipantSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + participant = ChatroomParticipant.objects.create( + chatroom=chatroom, + user_identifier='user123', + role='user', + metadata={'key': 'value'} + ) + + serializer = ChatroomParticipantSerializer(participant) + data = serializer.data + + expected_fields = ['uuid', 'user_identifier', 'role', 'metadata'] + for field in expected_fields: + assert field in data + + +@pytest.mark.unit +class TestChatRoomPreviewSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + + serializer = ChatRoomPreviewSerializer(chatroom) + data = serializer.data + + expected_fields = ['uuid', 'name', 'last_message', 'has_unread'] + for field in expected_fields: + assert field in data + + def test_get_last_message_dashboard_user(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + + serializer = ChatRoomPreviewSerializer( + chatroom, + context={'user_identifier': 'dashboard_user123'} + ) + last_message = serializer.get_last_message(chatroom) + + assert last_message is None + + def test_get_last_message_widget_user_filters_internal(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + + serializer = ChatRoomPreviewSerializer( + chatroom, + context={'user_identifier': 'widget_user123'} + ) + last_message = serializer.get_last_message(chatroom) + + assert last_message is None + + def test_get_has_unread_with_participant(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + ChatroomParticipant.objects.create( + chatroom=chatroom, + user_identifier='user123', + has_unread=True + ) + + serializer = ChatRoomPreviewSerializer( + chatroom, + context={'user_identifier': 'user123'} + ) + has_unread = serializer.get_has_unread(chatroom) + + assert has_unread is True + + def test_get_has_unread_without_participant(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + + serializer = ChatRoomPreviewSerializer( + chatroom, + context={'user_identifier': 'user123'} + ) + has_unread = serializer.get_has_unread(chatroom) + + assert has_unread is False + + def test_get_has_unread_without_user_identifier(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + + serializer = ChatRoomPreviewSerializer(chatroom) + has_unread = serializer.get_has_unread(chatroom) + + assert has_unread is False + + +@pytest.mark.unit +class TestChatRoomWithMessagesSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + ai_provider = AIProviderFactory(creator=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application, + ai_provider=ai_provider, + model='gpt-4' + ) + + serializer = ChatRoomWithMessagesSerializer(chatroom) + data = serializer.data + + expected_fields = ['uuid', 'name', 'application', 'messages', 'ai_provider', 'ai_model'] + for field in expected_fields: + assert field in data + + def test_get_messages_uses_context_messages_qs(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + + mock_messages_qs = Mock() + mock_messages_qs.all.return_value = [] + mock_messages_qs.__iter__ = Mock(return_value=iter([])) + serializer = ChatRoomWithMessagesSerializer( + chatroom, + context={'messages_qs': mock_messages_qs} + ) + messages = serializer.get_messages(chatroom) + + assert messages == [] + + +@pytest.mark.unit +class TestChatRoomDetailSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + + serializer = ChatRoomDetailSerializer(chatroom) + data = serializer.data + + expected_fields = ['uuid', 'name', 'participants', 'messages'] + for field in expected_fields: + assert field in data + + def test_serialization_includes_nested_participants(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + chatroom = ChatRoom.objects.create( + name='Test Chat', + application=application + ) + ChatroomParticipant.objects.create( + chatroom=chatroom, + user_identifier='user123', + role='user' + ) + + serializer = ChatRoomDetailSerializer(chatroom) + data = serializer.data + + assert 'participants' in data + assert len(data['participants']) == 1 diff --git a/backend/core/tests/test_serializers/test_configure_app_serializer.py b/backend/core/tests/test_serializers/test_configure_app_serializer.py new file mode 100644 index 0000000..16e1195 --- /dev/null +++ b/backend/core/tests/test_serializers/test_configure_app_serializer.py @@ -0,0 +1,185 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.models import Application, NotificationProfile, AppNotificationProfile, Integration +from core.serializers.configure_app import ( + LoadAppConfigurationSerializer, + ConfigureAppIntegrationSerializer, +) +from core.tests.factories import UserFactory, ApplicationFactory, NotificationProfileFactory, IntegrationFactory + + +@pytest.mark.unit +class TestLoadAppConfigurationSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + mock_request = Mock() + mock_request.user = user + + serializer = LoadAppConfigurationSerializer(application, context={'request': mock_request}) + data = serializer.data + + expected_fields = ['id', 'uuid', 'name', 'llm_models', 'integrations', 'notification_profiles'] + for field in expected_fields: + assert field in data + + def test_get_notification_profiles_returns_user_profiles(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + profile = NotificationProfileFactory(owner=user) + + mock_request = Mock() + mock_request.user = user + + serializer = LoadAppConfigurationSerializer(application, context={'request': mock_request}) + profiles = serializer.get_notification_profiles(application) + + assert len(profiles) == 1 + assert profiles[0]['id'] == profile.id + + def test_get_notification_profiles_includes_is_enabled(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + profile = NotificationProfileFactory(owner=user) + AppNotificationProfile.objects.create( + application=application, + notification_profile=profile + ) + + mock_request = Mock() + mock_request.user = user + + serializer = LoadAppConfigurationSerializer(application, context={'request': mock_request}) + profiles = serializer.get_notification_profiles(application) + + assert profiles[0]['is_enabled'] is True + + def test_get_notification_profiles_is_enabled_false_when_not_configured(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + profile = NotificationProfileFactory(owner=user) + + mock_request = Mock() + mock_request.user = user + + serializer = LoadAppConfigurationSerializer(application, context={'request': mock_request}) + profiles = serializer.get_notification_profiles(application) + + assert profiles[0]['is_enabled'] is False + + +@pytest.mark.unit +class TestConfigureAppIntegrationSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + mock_request = Mock() + mock_request.user = user + + serializer = ConfigureAppIntegrationSerializer(context={'request': mock_request, 'application': application}) + expected_fields = ['integration', 'type', 'branch_name'] + for field in expected_fields: + assert field in serializer.fields + + def test_init_filters_queryset_for_user(self): + user = UserFactory() + user_integration = IntegrationFactory(creator=user) + other_user = UserFactory() + other_integration = IntegrationFactory(creator=other_user) + + mock_request = Mock() + mock_request.user = user + + serializer = ConfigureAppIntegrationSerializer(context={'request': mock_request}) + assert user_integration in serializer.fields['integration'].queryset + assert other_integration not in serializer.fields['integration'].queryset + + def test_validate_integration_owner_matches_application_owner(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + + serializer = ConfigureAppIntegrationSerializer(context={'application': application}) + result = serializer.validate_integration(integration) + + assert result == integration + + def test_validate_integration_owner_mismatch(self): + user_a = UserFactory() + user_b = UserFactory() + application = ApplicationFactory(owner=user_a) + integration = IntegrationFactory(creator=user_b) + + serializer = ConfigureAppIntegrationSerializer(context={'application': application}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_integration(integration) + + assert "owner mismatch" in str(exc_info.value) + + def test_validate_integration_type_matches(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user, provider='github') + + attrs = { + 'integration': integration, + 'type': 'version_control' + } + + serializer = ConfigureAppIntegrationSerializer(context={'application': application}) + result = serializer.validate(attrs) + + assert result == attrs + + def test_validate_branch_name_required_for_github_pms(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user, provider='github') + + attrs = { + 'integration': integration, + 'type': 'pms', + 'branch_name': '' + } + + serializer = ConfigureAppIntegrationSerializer(context={'application': application}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate(attrs) + + assert "Branch is required" in str(exc_info.value) + + def test_validate_branch_name_not_required_for_non_pms(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user, provider='github') + + attrs = { + 'integration': integration, + 'type': 'version_control', + 'branch_name': '' + } + + serializer = ConfigureAppIntegrationSerializer(context={'application': application}) + result = serializer.validate(attrs) + + assert result == attrs + + def test_validate_branch_name_not_required_for_non_github(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user, provider='jira') + + attrs = { + 'integration': integration, + 'type': 'pms', + 'branch_name': '' + } + + serializer = ConfigureAppIntegrationSerializer(context={'application': application}) + result = serializer.validate(attrs) + + assert result == attrs diff --git a/backend/core/tests/test_serializers/test_integration_serializer.py b/backend/core/tests/test_serializers/test_integration_serializer.py new file mode 100644 index 0000000..d45a448 --- /dev/null +++ b/backend/core/tests/test_serializers/test_integration_serializer.py @@ -0,0 +1,245 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock, patch +import json + +from core.models import Integration +from core.serializers.integration import ( + IntegrationSerializer, + IntegrationViewSerializer, + IntegrationCreateSerializer, +) +from core.tests.factories import UserFactory, IntegrationFactory + + +@pytest.mark.unit +class TestIntegrationSerializer: + def test_serialization_excludes_credentials(self): + user = UserFactory() + integration = IntegrationFactory(creator=user) + + serializer = IntegrationSerializer(integration) + data = serializer.data + + assert 'credentials' not in data + + def test_serialization_includes_expected_fields(self): + user = UserFactory() + integration = IntegrationFactory(creator=user) + + serializer = IntegrationSerializer(integration) + data = serializer.data + + expected_fields = ['id', 'uuid', 'name', 'provider', 'creator', 'metadata', 'created_at', 'updated_at', 'supported_types'] + for field in expected_fields: + assert field in data + + def test_get_supported_types_for_supported_provider(self): + user = UserFactory() + integration = IntegrationFactory(creator=user, provider='github') + + serializer = IntegrationSerializer(integration) + supported_types = serializer.get_supported_types(integration) + + assert isinstance(supported_types, list) + + def test_get_supported_types_for_unsupported_provider(self): + user = UserFactory() + integration = IntegrationFactory(creator=user, provider='unknown_provider') + + serializer = IntegrationSerializer(integration) + supported_types = serializer.get_supported_types(integration) + + assert supported_types == [] + + +@pytest.mark.unit +class TestIntegrationViewSerializer: + + def test_is_alias_of_integration_serializer(self): + assert IntegrationViewSerializer == IntegrationSerializer + + +@pytest.mark.unit +class TestIntegrationCreateSerializer: + + def test_serialization_includes_expected_fields(self): + serializer = IntegrationCreateSerializer() + expected_fields = ['uuid', 'name', 'provider', 'token', 'creator'] + for field in expected_fields: + assert field in serializer.fields + + def test_token_is_write_only(self): + serializer = IntegrationCreateSerializer() + assert serializer.fields['token'].write_only + + def test_token_is_required_on_create(self): + serializer = IntegrationCreateSerializer() + assert serializer.fields['token'].required + + def test_init_makes_token_optional_on_update(self): + user = UserFactory() + integration = IntegrationFactory(creator=user) + + serializer = IntegrationCreateSerializer(instance=integration) + assert not serializer.fields['token'].required + assert serializer.fields['token'].allow_blank + + def test_init_makes_provider_read_only_on_update(self): + user = UserFactory() + integration = IntegrationFactory(creator=user) + + serializer = IntegrationCreateSerializer(instance=integration) + assert serializer.fields['provider'].read_only + + def test_validate_provider_supported(self): + serializer = IntegrationCreateSerializer() + result = serializer.validate_provider('github') + + assert result == 'github' + + def test_validate_provider_unsupported(self): + serializer = IntegrationCreateSerializer() + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_provider('unsupported_provider') + + assert "not supported" in str(exc_info.value) + + def test_validate_with_token_and_supported_provider(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + with patch('core.serializers.integration.importlib.import_module') as mock_import: + mock_validator = Mock(return_value=(True, None, {'account_id': '123'})) + mock_module = Mock() + mock_module.validate_token = mock_validator + mock_import.return_value = mock_module + + serializer = IntegrationCreateSerializer(context={'request': mock_request}) + attrs = { + 'provider': 'github', + 'token': 'valid_token' + } + + with patch('core.serializers.integration.SUPPORTED_INTEGRATIONS', [{'id': 'github', 'validate': 'module.validate_token'}]): + result = serializer.validate(attrs) + + assert result == attrs + + def test_validate_with_invalid_token(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + with patch('core.serializers.integration.importlib.import_module') as mock_import: + mock_validator = Mock(return_value=(False, 'Invalid token', None)) + mock_module = Mock() + mock_module.validate_token = mock_validator + mock_import.return_value = mock_module + + serializer = IntegrationCreateSerializer(context={'request': mock_request}) + attrs = { + 'provider': 'github', + 'token': 'invalid_token' + } + + with patch('core.serializers.integration.SUPPORTED_INTEGRATIONS', [{'id': 'github', 'validate': 'module.validate_token'}]): + result = serializer.validate(attrs) + + assert result == attrs + assert hasattr(serializer, '_credential_error') + assert serializer._credential_error == 'Invalid token' + + def test_create_stores_token_in_credentials(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + data = { + 'name': 'Test Integration', + 'provider': 'github', + 'token': 'test_token' + } + + serializer = IntegrationCreateSerializer(data=data, context={'request': mock_request}) + assert serializer.is_valid() + integration = serializer.save() + + credentials = json.loads(integration.credentials) + assert credentials == {'token': 'test_token'} + assert integration.creator == user + + def test_create_merges_account_metadata(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + serializer = IntegrationCreateSerializer(context={'request': mock_request}) + serializer._account_metadata = {'account_id': '123', 'username': 'testuser'} + + data = { + 'name': 'Test Integration', + 'provider': 'github', + 'token': 'test_token' + } + + serializer = IntegrationCreateSerializer(data=data, context={'request': mock_request}) + serializer._account_metadata = {'account_id': '123', 'username': 'testuser'} + assert serializer.is_valid() + integration = serializer.save() + + assert 'account' in integration.metadata + assert integration.metadata['account'] == {'account_id': '123', 'username': 'testuser'} + + def test_update_with_new_token(self): + user = UserFactory() + integration = IntegrationFactory(creator=user) + mock_request = Mock() + mock_request.user = user + + data = { + 'token': 'new_token' + } + + serializer = IntegrationCreateSerializer(instance=integration, data=data, partial=True, context={'request': mock_request}) + assert serializer.is_valid() + updated = serializer.save() + + credentials = json.loads(updated.credentials) + assert credentials == {'token': 'new_token'} + + def test_update_without_token(self): + user = UserFactory() + integration = IntegrationFactory(creator=user) + original_credentials = integration.credentials + mock_request = Mock() + mock_request.user = user + + data = { + 'name': 'Updated Name' + } + + serializer = IntegrationCreateSerializer(instance=integration, data=data, partial=True, context={'request': mock_request}) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.credentials == original_credentials + + def test_update_merges_account_metadata(self): + user = UserFactory() + integration = IntegrationFactory(creator=user) + mock_request = Mock() + mock_request.user = user + + data = { + 'token': 'new_token' + } + + serializer = IntegrationCreateSerializer(instance=integration, data=data, partial=True, context={'request': mock_request}) + serializer._account_metadata = {'account_id': '456'} + assert serializer.is_valid() + updated = serializer.save() + + assert 'account' in updated.metadata + assert updated.metadata['account'] == {'account_id': '456'} diff --git a/backend/core/tests/test_serializers/test_knowledge_base_serializer.py b/backend/core/tests/test_serializers/test_knowledge_base_serializer.py new file mode 100644 index 0000000..7345b0f --- /dev/null +++ b/backend/core/tests/test_serializers/test_knowledge_base_serializer.py @@ -0,0 +1,366 @@ +import pytest +from rest_framework import serializers + +from core.models import KnowledgeBase +from core.serializers.knowledge_base import ( + CrawlingConfigSerializer, + KnowledgeBaseItemSerializer, + KnowledgeBaseCreateSerializer, + KnowledgeBaseViewSerializer, + CrawlingEnableSerializer, + CrawlingStatsSerializer, + CrawledPageSerializer, + CrawlingDataSerializer, +) +from core.tests.factories import UserFactory, ApplicationFactory + + +@pytest.mark.unit +class TestCrawlingConfigSerializer: + def test_serialization_includes_expected_fields(self): + serializer = CrawlingConfigSerializer() + expected_fields = ['max_depth', 'max_pages', 'enable_crawling'] + for field in expected_fields: + assert field in serializer.fields + + def test_max_depth_is_read_only(self): + serializer = CrawlingConfigSerializer() + assert serializer.fields['max_depth'].read_only + + def test_max_pages_is_read_only(self): + serializer = CrawlingConfigSerializer() + assert serializer.fields['max_pages'].read_only + + def test_enable_crawling_is_not_read_only(self): + serializer = CrawlingConfigSerializer() + assert not serializer.fields['enable_crawling'].read_only + + def test_default_values(self): + serializer = CrawlingConfigSerializer() + assert serializer.fields['max_depth'].default == 1 + assert serializer.fields['max_pages'].default == 50 + + +@pytest.mark.unit +class TestKnowledgeBaseItemSerializer: + def test_serialization_includes_expected_fields(self): + serializer = KnowledgeBaseItemSerializer() + expected_fields = ['type', 'value', 'file', 'crawling_config'] + for field in expected_fields: + assert field in serializer.fields + + def test_type_has_correct_choices(self): + serializer = KnowledgeBaseItemSerializer() + assert set(serializer.fields['type'].choices.keys()) == {'file', 'text', 'url'} + + def test_value_is_optional(self): + serializer = KnowledgeBaseItemSerializer() + assert not serializer.fields['value'].required + assert serializer.fields['value'].allow_null + + def test_file_is_optional(self): + serializer = KnowledgeBaseItemSerializer() + assert not serializer.fields['file'].required + assert serializer.fields['file'].allow_null + + def test_validate_file_type_with_value(self): + data = { + 'type': 'file', + 'value': 'test_value' + } + serializer = KnowledgeBaseItemSerializer(data=data) + assert serializer.is_valid() + + def test_validate_file_type_without_value_or_file(self): + data = { + 'type': 'file' + } + serializer = KnowledgeBaseItemSerializer(data=data) + assert not serializer.is_valid() + assert 'file' in serializer.errors + + def test_validate_text_type_with_value(self): + data = { + 'type': 'text', + 'value': 'test_text' + } + serializer = KnowledgeBaseItemSerializer(data=data) + assert serializer.is_valid() + + def test_validate_text_type_without_value(self): + data = { + 'type': 'text' + } + serializer = KnowledgeBaseItemSerializer(data=data) + assert not serializer.is_valid() + assert 'value' in serializer.errors + + def test_validate_url_type_with_value(self): + data = { + 'type': 'url', + 'value': 'https://example.com' + } + serializer = KnowledgeBaseItemSerializer(data=data) + assert serializer.is_valid() + + def test_validate_url_type_without_value(self): + data = { + 'type': 'url' + } + serializer = KnowledgeBaseItemSerializer(data=data) + assert not serializer.is_valid() + assert 'value' in serializer.errors + + +@pytest.mark.unit +class TestKnowledgeBaseCreateSerializer: + def test_serialization_includes_expected_fields(self): + serializer = KnowledgeBaseCreateSerializer() + expected_fields = ['items'] + for field in expected_fields: + assert field in serializer.fields + + def test_items_is_many(self): + serializer = KnowledgeBaseCreateSerializer() + assert serializer.fields['items'].many + + def test_create_returns_validated_data(self): + data = { + 'items': [ + {'type': 'text', 'value': 'test'} + ] + } + serializer = KnowledgeBaseCreateSerializer(data=data) + assert serializer.is_valid() + result = serializer.save() + assert result == data + + +@pytest.mark.unit +class TestKnowledgeBaseViewSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create(application=application) + + serializer = KnowledgeBaseViewSerializer(kb) + data = serializer.data + + expected_fields = ['id', 'uuid', 'application_id', 'path', 'metadata', 'source_type', 'status', 'created_at', + 'crawling_enabled', 'crawling_status', 'crawled_pages', 'crawling_config'] + for field in expected_fields: + assert field in data + + def test_get_crawling_enabled_for_url_with_crawling_enabled(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='url', + metadata={'crawling_enabled': True} + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawling_enabled(kb) + + assert result is True + + def test_get_crawling_enabled_for_url_without_crawling_enabled(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='url', + metadata={} + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawling_enabled(kb) + + assert result is False + + def test_get_crawling_enabled_for_non_url(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='file' + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawling_enabled(kb) + + assert result is False + + def test_get_crawling_status_for_url(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='url', + metadata={'crawling_status': 'in_progress'} + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawling_status(kb) + + assert result == 'in_progress' + + def test_get_crawling_status_for_non_url(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='file' + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawling_status(kb) + + assert result is None + + def test_get_crawled_pages_for_url(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='url', + metadata={'crawled_data': {'total_pages': 10}} + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawled_pages(kb) + + assert result == 10 + + def test_get_crawled_pages_for_non_url(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='file' + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawled_pages(kb) + + assert result == 0 + + def test_get_crawling_config_for_url(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='url', + metadata={'crawling_config': {'max_depth': 2}} + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawling_config(kb) + + assert result == {'max_depth': 2} + + def test_get_crawling_config_for_non_url(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + kb = KnowledgeBase.objects.create( + application=application, + source_type='file' + ) + + serializer = KnowledgeBaseViewSerializer(kb) + result = serializer.get_crawling_config(kb) + + assert result == {} + + +@pytest.mark.unit +class TestCrawlingEnableSerializer: + def test_serialization_includes_expected_fields(self): + serializer = CrawlingEnableSerializer() + expected_fields = ['max_depth', 'max_pages'] + for field in expected_fields: + assert field in serializer.fields + + def test_max_depth_is_read_only(self): + serializer = CrawlingEnableSerializer() + assert serializer.fields['max_depth'].read_only + + def test_max_pages_is_read_only(self): + serializer = CrawlingEnableSerializer() + assert serializer.fields['max_pages'].read_only + + def test_default_values(self): + serializer = CrawlingEnableSerializer() + assert serializer.fields['max_depth'].default == 1 + assert serializer.fields['max_pages'].default == 50 + + +@pytest.mark.unit +class TestCrawlingStatsSerializer: + def test_serialization_includes_expected_fields(self): + serializer = CrawlingStatsSerializer() + expected_fields = ['total_pages', 'total_urls_visited', 'total_urls_encountered', 'success_rate', + 'max_depth_reached', 'deduplication_stats', 'relationship_stats'] + for field in expected_fields: + assert field in serializer.fields + + def test_all_fields_are_required(self): + serializer = CrawlingStatsSerializer() + for field_name, field in serializer.fields.items(): + assert field.required + + +@pytest.mark.unit +class TestCrawledPageSerializer: + def test_serialization_includes_expected_fields(self): + serializer = CrawledPageSerializer() + expected_fields = ['url', 'title', 'description', 'content_length', 'depth', 'status_code', + 'content_type', 'parent_url', 'retry_attempts'] + for field in expected_fields: + assert field in serializer.fields + + def test_url_is_required(self): + serializer = CrawledPageSerializer() + assert serializer.fields['url'].required + + def test_title_is_optional(self): + serializer = CrawledPageSerializer() + assert serializer.fields['title'].allow_null + + def test_description_is_optional(self): + serializer = CrawledPageSerializer() + assert serializer.fields['description'].allow_null + + def test_content_type_is_optional(self): + serializer = CrawledPageSerializer() + assert serializer.fields['content_type'].allow_null + + def test_parent_url_is_optional(self): + serializer = CrawledPageSerializer() + assert serializer.fields['parent_url'].allow_null + + def test_retry_attempts_has_default(self): + serializer = CrawledPageSerializer() + assert serializer.fields['retry_attempts'].default == 0 + + +@pytest.mark.unit +class TestCrawlingDataSerializer: + def test_serialization_includes_expected_fields(self): + serializer = CrawlingDataSerializer() + expected_fields = ['total_pages', 'pages', 'crawl_stats'] + for field in expected_fields: + assert field in serializer.fields + + def test_total_pages_is_required(self): + serializer = CrawlingDataSerializer() + assert serializer.fields['total_pages'].required + + def test_pages_is_many(self): + serializer = CrawlingDataSerializer() + assert serializer.fields['pages'].many + + def test_crawl_stats_is_required(self): + serializer = CrawlingDataSerializer() + assert serializer.fields['crawl_stats'].required diff --git a/backend/core/tests/test_serializers/test_llm_model_serializer.py b/backend/core/tests/test_serializers/test_llm_model_serializer.py new file mode 100644 index 0000000..c04c591 --- /dev/null +++ b/backend/core/tests/test_serializers/test_llm_model_serializer.py @@ -0,0 +1,164 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.models import LLMModel +from core.serializers.llm_model import ( + LLMModelViewSerializer, + LLMModelCreateSerializer, +) +from core.tests.factories import UserFactory + + +@pytest.mark.unit +class TestLLMModelViewSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + base_url='https://api.example.com', + model_name='gpt-4', + model_type='text' + ) + + serializer = LLMModelViewSerializer(llm_model) + data = serializer.data + + expected_fields = ['id', 'uuid', 'name', 'base_url', 'model_name', 'model_type', 'is_default', 'created_at', 'owner'] + for field in expected_fields: + assert field in data + + def test_serialization_excludes_api_key(self): + user = UserFactory() + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + base_url='https://api.example.com', + model_name='gpt-4', + model_type='text' + ) + + serializer = LLMModelViewSerializer(llm_model) + data = serializer.data + + assert 'api_key' not in data + + +@pytest.mark.unit +class TestLLMModelCreateSerializer: + def test_serialization_includes_expected_fields(self): + serializer = LLMModelCreateSerializer() + expected_fields = ['name', 'api_key', 'base_url', 'model_name', 'model_type'] + for field in expected_fields: + assert field in serializer.fields + + def test_api_key_is_write_only(self): + serializer = LLMModelCreateSerializer() + assert serializer.fields['api_key'].write_only + + def test_api_key_is_optional(self): + serializer = LLMModelCreateSerializer() + assert not serializer.fields['api_key'].required + assert serializer.fields['api_key'].allow_blank + assert serializer.fields['api_key'].allow_null + + def test_create_with_api_key(self): + user = UserFactory() + data = { + 'name': 'Test Model', + 'api_key': 'test_api_key', + 'base_url': 'https://api.example.com', + 'model_name': 'gpt-4', + 'model_type': 'text' + } + + serializer = LLMModelCreateSerializer(data=data) + assert serializer.is_valid() + llm_model = serializer.save(owner=user) + + assert llm_model.name == 'Test Model' + assert llm_model.base_url == 'https://api.example.com' + assert llm_model.model_name == 'gpt-4' + assert llm_model.model_type == 'text' + assert llm_model.config == 'test_api_key' + + def test_create_without_api_key(self): + user = UserFactory() + data = { + 'name': 'Test Model', + 'base_url': 'https://api.example.com', + 'model_name': 'gpt-4', + 'model_type': 'text' + } + + serializer = LLMModelCreateSerializer(data=data) + assert serializer.is_valid() + llm_model = serializer.save(owner=user) + + assert llm_model.name == 'Test Model' + assert llm_model.base_url == 'https://api.example.com' + assert llm_model.model_name == 'gpt-4' + assert llm_model.model_type == 'text' + + def test_update_with_api_key(self): + user = UserFactory() + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + base_url='https://api.example.com', + model_name='gpt-4', + model_type='text' + ) + + data = { + 'name': 'Updated Model', + 'api_key': 'new_api_key' + } + + serializer = LLMModelCreateSerializer(instance=llm_model, data=data, partial=True) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.name == 'Updated Model' + assert updated.config == 'new_api_key' + + def test_update_without_api_key(self): + user = UserFactory() + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + base_url='https://api.example.com', + model_name='gpt-4', + model_type='text' + ) + + data = { + 'name': 'Updated Model' + } + + serializer = LLMModelCreateSerializer(instance=llm_model, data=data, partial=True) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.name == 'Updated Model' + + def test_update_with_null_api_key(self): + user = UserFactory() + llm_model = LLMModel.objects.create( + owner=user, + name='Test Model', + base_url='https://api.example.com', + model_name='gpt-4', + model_type='text' + ) + + data = { + 'api_key': None + } + + serializer = LLMModelCreateSerializer(instance=llm_model, data=data, partial=True) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.config == {} or updated.config == '' diff --git a/backend/core/tests/test_serializers/test_message_serializer.py b/backend/core/tests/test_serializers/test_message_serializer.py new file mode 100644 index 0000000..f4becdb --- /dev/null +++ b/backend/core/tests/test_serializers/test_message_serializer.py @@ -0,0 +1,121 @@ +import pytest +from rest_framework import serializers + +from core.models import Message +from core.serializers.message import ( + CreateMessageSerializer, + ViewMessageSerializer, +) +from core.tests.factories import UserFactory, ApplicationFactory, AIProviderFactory + + +@pytest.mark.unit +class TestCreateMessageSerializer: + def test_serialization_includes_expected_fields(self): + serializer = CreateMessageSerializer() + expected_fields = ['chatroom_identifier', 'sender_identifier', 'message', 'metadata', 'is_internal', 'ai_mode', 'ai_provider', 'model'] + for field in expected_fields: + assert field in serializer.fields + + def test_chatroom_identifier_is_optional(self): + serializer = CreateMessageSerializer() + assert not serializer.fields['chatroom_identifier'].required + + def test_sender_identifier_is_optional(self): + serializer = CreateMessageSerializer() + assert not serializer.fields['sender_identifier'].required + + def test_message_is_required(self): + serializer = CreateMessageSerializer() + assert serializer.fields['message'].required + + def test_metadata_is_optional(self): + serializer = CreateMessageSerializer() + assert not serializer.fields['metadata'].required + + def test_is_internal_has_default(self): + serializer = CreateMessageSerializer() + assert serializer.fields['is_internal'].default == False + + def test_ai_mode_has_default(self): + serializer = CreateMessageSerializer() + assert serializer.fields['ai_mode'].default == False + + def test_ai_provider_is_optional(self): + serializer = CreateMessageSerializer() + assert not serializer.fields['ai_provider'].required + + def test_model_is_optional(self): + serializer = CreateMessageSerializer() + assert not serializer.fields['model'].required + + def test_init_sets_app_owner(self): + user = UserFactory() + serializer = CreateMessageSerializer(app_owner=user) + assert serializer.app_owner == user + + def test_validate_ai_provider_valid(self): + user = UserFactory() + ai_provider = AIProviderFactory(creator=user) + + serializer = CreateMessageSerializer(app_owner=user) + result = serializer.validate_ai_provider(ai_provider.id) + + assert result == ai_provider.id + + def test_validate_ai_provider_invalid(self): + user = UserFactory() + other_user = UserFactory() + ai_provider = AIProviderFactory(creator=other_user) + + serializer = CreateMessageSerializer(app_owner=user) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_ai_provider(ai_provider.id) + + assert "Invalid AI provider" in str(exc_info.value) + + def test_validate_ai_provider_none(self): + user = UserFactory() + + serializer = CreateMessageSerializer(app_owner=user) + result = serializer.validate_ai_provider(None) + + assert result is None + + +@pytest.mark.unit +class TestViewMessageSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + from core.models import ChatRoom + chatroom = ChatRoom.objects.create(application=application, name='Test Chat') + message = Message.objects.create( + chatroom=chatroom, + message='Test message', + sender_identifier='user123' + ) + + serializer = ViewMessageSerializer(message) + data = serializer.data + + expected_fields = ['id', 'uuid', 'sender_identifier', 'chatroom_identifier', 'message', 'metadata', 'ai_provider_id', 'model', 'is_internal', 'platform', 'ai_mode', 'created_at'] + for field in expected_fields: + assert field in data + + def test_chatroom_identifier_is_uuid(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + from core.models import ChatRoom + chatroom = ChatRoom.objects.create(application=application, name='Test Chat') + message = Message.objects.create( + chatroom=chatroom, + message='Test message', + sender_identifier='user123' + ) + + serializer = ViewMessageSerializer(message) + data = serializer.data + + assert 'chatroom_identifier' in data + assert data['chatroom_identifier'] == str(chatroom.uuid) diff --git a/backend/core/tests/test_serializers/test_notification_profiles_serializer.py b/backend/core/tests/test_serializers/test_notification_profiles_serializer.py new file mode 100644 index 0000000..a9d8152 --- /dev/null +++ b/backend/core/tests/test_serializers/test_notification_profiles_serializer.py @@ -0,0 +1,148 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.models import NotificationProfile +from core.serializers.notification_profiles import NotificationProfileSerializer +from core.tests.factories import UserFactory, NotificationProfileFactory + + +@pytest.mark.unit +class TestNotificationProfileSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + profile = NotificationProfileFactory(owner=user) + + serializer = NotificationProfileSerializer(profile) + data = serializer.data + + expected_fields = ['id', 'uuid', 'type', 'config', 'created_at', 'name', 'owner'] + for field in expected_fields: + assert field in data + + def test_read_only_fields(self): + serializer = NotificationProfileSerializer() + assert serializer.fields['id'].read_only + assert serializer.fields['created_at'].read_only + assert serializer.fields['owner'].read_only + + def test_create_with_config(self): + user = UserFactory() + mock_request = Mock() + mock_request.user = user + + data = { + 'name': 'Test Profile', + 'type': 'email', + 'config': {'email': 'test@example.com'} + } + + serializer = NotificationProfileSerializer(data=data) + assert serializer.is_valid() + profile = serializer.save(owner=user) + + assert profile.name == 'Test Profile' + assert profile.type == 'email' + assert profile.config == {'email': 'test@example.com'} + + def test_create_without_config(self): + user = UserFactory() + + data = { + 'name': 'Test Profile', + 'type': 'email', + 'config': {} + } + + serializer = NotificationProfileSerializer(data=data) + assert serializer.is_valid() + profile = serializer.save(owner=user) + + assert profile.name == 'Test Profile' + assert profile.type == 'email' + + def test_update_with_config_email(self): + user = UserFactory() + profile = NotificationProfileFactory(owner=user, type='email') + + data = { + 'config': {'email': 'new@example.com'} + } + + serializer = NotificationProfileSerializer(instance=profile, data=data, partial=True) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.config == {'email': 'new@example.com'} + + def test_update_with_config_webhookUrl(self): + user = UserFactory() + profile = NotificationProfileFactory(owner=user, type='slack') + + data = { + 'config': {'webhookUrl': 'https://hooks.slack.com/test'} + } + + serializer = NotificationProfileSerializer(instance=profile, data=data, partial=True) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.config == {'webhookUrl': 'https://hooks.slack.com/test'} + + def test_update_with_config_webhook_url(self): + user = UserFactory() + profile = NotificationProfileFactory(owner=user, type='slack') + + data = { + 'config': {'webhook_url': 'https://hooks.slack.com/test'} + } + + serializer = NotificationProfileSerializer(instance=profile, data=data, partial=True) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.config == {'webhookUrl': 'https://hooks.slack.com/test'} + + def test_update_without_config(self): + user = UserFactory() + profile = NotificationProfileFactory(owner=user, name='Old Name') + + data = { + 'name': 'New Name' + } + + serializer = NotificationProfileSerializer(instance=profile, data=data, partial=True) + assert serializer.is_valid() + updated = serializer.save() + + assert updated.name == 'New Name' + + def test_to_representation_returns_safe_config_email(self): + user = UserFactory() + profile = NotificationProfileFactory(owner=user, type='email') + + serializer = NotificationProfileSerializer(profile) + data = serializer.data + + assert 'config' in data + assert data['config'] == {'email': profile.config.get('email')} + + def test_to_representation_returns_safe_config_webhook(self): + user = UserFactory() + profile = NotificationProfileFactory(owner=user, type='slack') + + serializer = NotificationProfileSerializer(profile) + data = serializer.data + + assert 'config' in data + assert 'webhookUrl' in data['config'] + + def test_to_representation_handles_empty_config(self): + user = UserFactory() + profile = NotificationProfileFactory(owner=user, type='email') + + serializer = NotificationProfileSerializer(profile) + data = serializer.data + + assert 'config' in data + assert data['config'] == {'email': profile.config.get('email')} diff --git a/backend/core/tests/test_serializers/test_password_serializer.py b/backend/core/tests/test_serializers/test_password_serializer.py new file mode 100644 index 0000000..eb99011 --- /dev/null +++ b/backend/core/tests/test_serializers/test_password_serializer.py @@ -0,0 +1,78 @@ +import pytest +from rest_framework import serializers +from django.contrib.auth.models import User + +from core.serializers.password import ( + ForgotPasswordSerializer, + ResetPasswordSerializer, +) +from core.tests.factories import UserFactory + + +@pytest.mark.unit +class TestForgotPasswordSerializer: + def test_serialization_includes_expected_fields(self): + serializer = ForgotPasswordSerializer() + expected_fields = ['email'] + for field in expected_fields: + assert field in serializer.fields + + def test_email_is_required(self): + serializer = ForgotPasswordSerializer() + assert serializer.fields['email'].required + + def test_validate_email_valid(self): + user = UserFactory(email='test@example.com') + + serializer = ForgotPasswordSerializer() + result = serializer.validate_email('test@example.com') + + assert result == 'test@example.com' + + def test_validate_email_invalid(self): + serializer = ForgotPasswordSerializer() + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_email('nonexistent@example.com') + + assert "does not exist" in str(exc_info.value) + + +@pytest.mark.unit +class TestResetPasswordSerializer: + def test_serialization_includes_expected_fields(self): + serializer = ResetPasswordSerializer() + expected_fields = ['password', 'confirm_password'] + for field in expected_fields: + assert field in serializer.fields + + def test_password_is_write_only(self): + serializer = ResetPasswordSerializer() + assert serializer.fields['password'].write_only + + def test_confirm_password_is_write_only(self): + serializer = ResetPasswordSerializer() + assert serializer.fields['confirm_password'].write_only + + def test_password_min_length(self): + serializer = ResetPasswordSerializer() + assert serializer.fields['password'].min_length == 8 + + def test_validate_passwords_match(self): + data = { + 'password': 'newpassword123', + 'confirm_password': 'newpassword123' + } + + serializer = ResetPasswordSerializer(data=data) + assert serializer.is_valid() + + def test_validate_passwords_do_not_match(self): + data = { + 'password': 'newpassword123', + 'confirm_password': 'differentpassword' + } + + serializer = ResetPasswordSerializer(data=data) + assert not serializer.is_valid() + assert 'confirm_password' in serializer.errors + assert "must match" in str(serializer.errors['confirm_password']) diff --git a/backend/core/tests/test_serializers/test_prompt_config_serializer.py b/backend/core/tests/test_serializers/test_prompt_config_serializer.py new file mode 100644 index 0000000..79bd6cf --- /dev/null +++ b/backend/core/tests/test_serializers/test_prompt_config_serializer.py @@ -0,0 +1,154 @@ +import pytest +from rest_framework import serializers + +from core.serializers.prompt_config import PromptConfigSerializer + + +@pytest.mark.unit +class TestPromptConfigSerializer: + def test_serialization_includes_expected_fields(self): + serializer = PromptConfigSerializer() + expected_fields = ['tone', 'response_style', 'custom_instructions', 'role', 'behavior'] + for field in expected_fields: + assert field in serializer.fields + + def test_tone_has_correct_choices(self): + serializer = PromptConfigSerializer() + expected_choices = ["professional", "friendly", "formal", "casual"] + assert set(serializer.fields['tone'].choices) == set(expected_choices) + + def test_response_style_has_correct_choices(self): + serializer = PromptConfigSerializer() + expected_choices = ["balanced", "concise", "detailed", "step_by_step"] + assert set(serializer.fields['response_style'].choices) == set(expected_choices) + + def test_custom_instructions_max_length(self): + serializer = PromptConfigSerializer() + assert serializer.fields['custom_instructions'].max_length == 1000 + + def test_custom_instructions_allow_blank(self): + serializer = PromptConfigSerializer() + assert serializer.fields['custom_instructions'].allow_blank + + def test_custom_instructions_not_required(self): + serializer = PromptConfigSerializer() + assert not serializer.fields['custom_instructions'].required + + def test_custom_instructions_default(self): + serializer = PromptConfigSerializer() + assert serializer.fields['custom_instructions'].default == "" + + def test_role_max_length(self): + serializer = PromptConfigSerializer() + assert serializer.fields['role'].max_length == 200 + + def test_role_not_allow_blank(self): + serializer = PromptConfigSerializer() + assert not serializer.fields['role'].allow_blank + + def test_role_not_required(self): + serializer = PromptConfigSerializer() + assert not serializer.fields['role'].required + + def test_role_default(self): + serializer = PromptConfigSerializer() + assert serializer.fields['role'].default == "customer service assistant" + + def test_behavior_max_length(self): + serializer = PromptConfigSerializer() + assert serializer.fields['behavior'].max_length == 500 + + def test_behavior_not_allow_blank(self): + serializer = PromptConfigSerializer() + assert not serializer.fields['behavior'].allow_blank + + def test_behavior_not_required(self): + serializer = PromptConfigSerializer() + assert not serializer.fields['behavior'].required + + def test_behavior_default(self): + serializer = PromptConfigSerializer() + assert serializer.fields['behavior'].default == "answer user questions politely and competently" + + def test_validate_valid_tone(self): + data = { + 'tone': 'professional', + 'response_style': 'balanced' + } + + serializer = PromptConfigSerializer(data=data) + assert serializer.is_valid() + + def test_validate_invalid_tone(self): + data = { + 'tone': 'invalid_tone', + 'response_style': 'balanced' + } + + serializer = PromptConfigSerializer(data=data) + assert not serializer.is_valid() + assert 'tone' in serializer.errors + + def test_validate_valid_response_style(self): + data = { + 'tone': 'professional', + 'response_style': 'detailed' + } + + serializer = PromptConfigSerializer(data=data) + assert serializer.is_valid() + + def test_validate_invalid_response_style(self): + data = { + 'tone': 'professional', + 'response_style': 'invalid_style' + } + + serializer = PromptConfigSerializer(data=data) + assert not serializer.is_valid() + assert 'response_style' in serializer.errors + + def test_validate_with_all_fields(self): + data = { + 'tone': 'friendly', + 'response_style': 'detailed', + 'custom_instructions': 'Be extra helpful', + 'role': 'support agent', + 'behavior': 'Provide detailed explanations' + } + + serializer = PromptConfigSerializer(data=data) + assert serializer.is_valid() + + def test_validate_custom_instructions_too_long(self): + data = { + 'tone': 'professional', + 'response_style': 'balanced', + 'custom_instructions': 'x' * 1001 + } + + serializer = PromptConfigSerializer(data=data) + assert not serializer.is_valid() + assert 'custom_instructions' in serializer.errors + + def test_validate_role_too_long(self): + data = { + 'tone': 'professional', + 'response_style': 'balanced', + 'role': 'x' * 201 + } + + serializer = PromptConfigSerializer(data=data) + assert not serializer.is_valid() + assert 'role' in serializer.errors + + def test_validate_behavior_too_long(self): + data = { + 'tone': 'professional', + 'response_style': 'balanced', + 'behavior': 'x' * 501 + } + + serializer = PromptConfigSerializer(data=data) + assert not serializer.is_valid() + assert 'behavior' in serializer.errors diff --git a/backend/core/tests/test_serializers/test_user_serializer.py b/backend/core/tests/test_serializers/test_user_serializer.py new file mode 100644 index 0000000..0736a37 --- /dev/null +++ b/backend/core/tests/test_serializers/test_user_serializer.py @@ -0,0 +1,145 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock, patch + +from django.contrib.auth.models import User +from core.serializers.user import ( + UserRegisterSerializer, + UserViewSerializer, +) +from core.models import AccountStatus + + +@pytest.mark.unit +class TestUserRegisterSerializer: + def test_serialization_includes_expected_fields(self): + serializer = UserRegisterSerializer() + expected_fields = ['id', 'username', 'email', 'password'] + for field in expected_fields: + assert field in serializer.fields + + def test_password_is_write_only(self): + serializer = UserRegisterSerializer() + assert serializer.fields['password'].write_only + + def test_create_creates_user(self): + data = { + 'username': 'testuser', + 'email': 'test@example.com', + 'password': 'testpassword123' + } + + with patch('core.serializers.user.generate_verification_token') as mock_token, \ + patch('core.serializers.user.send_verification_email_task') as mock_email, \ + patch('core.serializers.user.send_discord_notification_task') as mock_discord: + mock_token.return_value = 'test_token' + + serializer = UserRegisterSerializer(data=data) + assert serializer.is_valid() + user = serializer.save() + + assert user.username == 'testuser' + assert user.email == 'test@example.com' + assert not user.is_active + + def test_create_creates_account_status(self): + data = { + 'username': 'testuser', + 'email': 'test@example.com', + 'password': 'testpassword123' + } + + with patch('core.serializers.user.generate_verification_token') as mock_token, \ + patch('core.serializers.user.send_verification_email_task') as mock_email, \ + patch('core.serializers.user.send_discord_notification_task') as mock_discord: + mock_token.return_value = 'test_token' + + serializer = UserRegisterSerializer(data=data) + assert serializer.is_valid() + user = serializer.save() + + account_status = AccountStatus.objects.get(account=user) + assert account_status.status == 'PENDING' + + def test_create_generates_verification_token(self): + data = { + 'username': 'testuser', + 'email': 'test@example.com', + 'password': 'testpassword123' + } + + with patch('core.serializers.user.generate_verification_token') as mock_token, \ + patch('core.serializers.user.send_verification_email_task') as mock_email, \ + patch('core.serializers.user.send_discord_notification_task') as mock_discord: + mock_token.return_value = 'test_token' + + serializer = UserRegisterSerializer(data=data) + assert serializer.is_valid() + serializer.save() + + mock_token.assert_called_once() + + def test_create_sends_verification_email(self): + data = { + 'username': 'testuser', + 'email': 'test@example.com', + 'password': 'testpassword123' + } + + with patch('core.serializers.user.generate_verification_token') as mock_token, \ + patch('core.serializers.user.send_verification_email_task') as mock_email, \ + patch('core.serializers.user.send_discord_notification_task') as mock_discord: + mock_token.return_value = 'test_token' + + serializer = UserRegisterSerializer(data=data) + assert serializer.is_valid() + user = serializer.save() + + mock_email.delay.assert_called_once() + + def test_create_sends_discord_notification(self): + data = { + 'username': 'testuser', + 'email': 'test@example.com', + 'password': 'testpassword123' + } + + with patch('core.serializers.user.generate_verification_token') as mock_token, \ + patch('core.serializers.user.send_verification_email_task') as mock_email, \ + patch('core.serializers.user.send_discord_notification_task') as mock_discord: + mock_token.return_value = 'test_token' + + serializer = UserRegisterSerializer(data=data) + assert serializer.is_valid() + serializer.save() + + mock_discord.delay.assert_called_once_with(f"A new user has registered: test@example.com") + + +@pytest.mark.unit +class TestUserViewSerializer: + def test_serialization_includes_expected_fields(self): + user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword123' + ) + + serializer = UserViewSerializer(user) + data = serializer.data + + expected_fields = ['id', 'username', 'email', 'first_name', 'last_name'] + for field in expected_fields: + assert field in data + + def test_serialization_excludes_password(self): + user = User.objects.create_user( + username='testuser', + email='test@example.com', + password='testpassword123' + ) + + serializer = UserViewSerializer(user) + data = serializer.data + + assert 'password' not in data diff --git a/backend/core/tests/test_serializers/test_version_control_serializers.py b/backend/core/tests/test_serializers/test_version_control_serializers.py new file mode 100644 index 0000000..4108d2d --- /dev/null +++ b/backend/core/tests/test_serializers/test_version_control_serializers.py @@ -0,0 +1,528 @@ +import pytest +from rest_framework import serializers +from unittest.mock import Mock + +from core.serializers.version_control_serializers import ( + VCRepositorySerializer, + VCIssueCommentSerializer, + VCIssueSerializer, + VCPRFileSerializer, + VCPRCommentSerializer, + VCPullRequestSerializer, + VCRepositoryDetailSerializer, + VCIngestionRequestSerializer, +) +from core.models.version_control import ( + VCRepository, VCIssue, VCIssueComment, VCPullRequest, + VCPRComment, VCPRFile +) +from core.tests.factories import UserFactory, ApplicationFactory, IntegrationFactory, AppIntegrationFactory + + +@pytest.mark.unit +class TestVCRepositorySerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + + serializer = VCRepositorySerializer(repo) + data = serializer.data + + expected_fields = ['id', 'uuid', 'provider', 'external_id', 'name', 'repo_owner', 'full_name', + 'description', 'url', 'is_private', 'default_branch', 'last_ingested_at', + 'ingestion_status', 'metadata', 'created_at', 'updated_at'] + for field in expected_fields: + assert field in data + + def test_read_only_fields(self): + serializer = VCRepositorySerializer() + assert serializer.fields['id'].read_only + assert serializer.fields['uuid'].read_only + assert serializer.fields['created_at'].read_only + assert serializer.fields['updated_at'].read_only + + +@pytest.mark.unit +class TestVCIssueCommentSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + issue = VCIssue.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test Issue', + author='testuser', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/issues/1' + ) + comment = VCIssueComment.objects.create( + issue=issue, + external_id='1', + body='Test comment', + author='testuser', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/issues/1#comment-1' + ) + + serializer = VCIssueCommentSerializer(comment) + data = serializer.data + + expected_fields = ['id', 'uuid', 'external_id', 'body', 'author', 'author_association', + 'url', 'created_at', 'updated_at'] + for field in expected_fields: + assert field in data + + +@pytest.mark.unit +class TestVCIssueSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + issue = VCIssue.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test Issue', + author='testuser', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/issues/1' + ) + + serializer = VCIssueSerializer(issue) + data = serializer.data + + expected_fields = ['id', 'uuid', 'external_id', 'number', 'title', 'body', 'state', + 'author', 'author_association', 'assignees', 'labels', 'milestone', + 'locked', 'closed_at', 'url', + 'comments', 'comment_count', 'created_at', 'updated_at'] + for field in expected_fields: + assert field in data + + def test_get_comment_count(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + issue = VCIssue.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test Issue', + author='testuser', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/issues/1' + ) + VCIssueComment.objects.create( + issue=issue, + external_id='1', + body='Test comment', + author='testuser', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/issues/1#comment-1' + ) + + serializer = VCIssueSerializer(issue) + comment_count = serializer.get_comment_count(issue) + + assert comment_count == 1 + + +@pytest.mark.unit +class TestVCPRFileSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + pr = VCPullRequest.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test PR', + author='testuser', + head_branch='feature', + base_branch='main', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/pull/1' + ) + pr_file = VCPRFile.objects.create( + pull_request=pr, + filename='test.py', + status='added' + ) + + serializer = VCPRFileSerializer(pr_file) + data = serializer.data + + expected_fields = ['id', 'uuid', 'filename', 'status', 'additions', 'deletions', + 'changes', 'patch', 'blob_url', 'raw_url', 'contents_url', + 'created_at', 'updated_at'] + for field in expected_fields: + assert field in data + + +@pytest.mark.unit +class TestVCPRCommentSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + pr = VCPullRequest.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test PR', + author='testuser', + head_branch='feature', + base_branch='main', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/pull/1' + ) + pr_comment = VCPRComment.objects.create( + pull_request=pr, + external_id='1', + body='Test comment', + author='testuser', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/pull/1#comment-1' + ) + + serializer = VCPRCommentSerializer(pr_comment) + data = serializer.data + + expected_fields = ['id', 'uuid', 'external_id', 'body', 'author', 'author_association', + 'url', 'created_at', 'updated_at'] + for field in expected_fields: + assert field in data + + +@pytest.mark.unit +class TestVCPullRequestSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + pr = VCPullRequest.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test PR', + author='testuser', + head_branch='feature', + base_branch='main', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/pull/1' + ) + + serializer = VCPullRequestSerializer(pr) + data = serializer.data + + expected_fields = ['id', 'uuid', 'external_id', 'number', 'title', 'body', 'state', + 'author', 'author_association', 'assignees', 'reviewers', 'labels', + 'milestone', 'head_branch', 'base_branch', 'merged', 'merged_at', + 'merge_commit_sha', 'additions', 'deletions', 'changed_files', + 'closed_at', 'url', + 'comments', 'files', 'comment_count', 'file_count', + 'created_at', 'updated_at'] + for field in expected_fields: + assert field in data + + def test_get_comment_count(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + pr = VCPullRequest.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test PR', + author='testuser', + head_branch='feature', + base_branch='main', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/pull/1' + ) + VCPRComment.objects.create( + pull_request=pr, + external_id='1', + body='Test comment', + author='testuser', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/pull/1#comment-1' + ) + + serializer = VCPullRequestSerializer(pr) + comment_count = serializer.get_comment_count(pr) + + assert comment_count == 1 + + def test_get_file_count(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + pr = VCPullRequest.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test PR', + author='testuser', + head_branch='feature', + base_branch='main', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/pull/1' + ) + VCPRFile.objects.create( + pull_request=pr, + filename='test.py', + status='added' + ) + + serializer = VCPullRequestSerializer(pr) + file_count = serializer.get_file_count(pr) + + assert file_count == 1 + + +@pytest.mark.unit +class TestVCRepositoryDetailSerializer: + def test_serialization_includes_expected_fields(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + + serializer = VCRepositoryDetailSerializer(repo) + data = serializer.data + + expected_fields = ['id', 'uuid', 'provider', 'external_id', 'name', 'repo_owner', 'full_name', + 'description', 'url', 'is_private', 'default_branch', 'last_ingested_at', + 'ingestion_status', 'metadata', 'created_at', 'updated_at', + 'issues', 'pull_requests', 'issue_count', 'pr_count'] + for field in expected_fields: + assert field in data + + def test_get_issue_count(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + VCIssue.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test Issue', + author='testuser', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/issues/1' + ) + + serializer = VCRepositoryDetailSerializer(repo) + issue_count = serializer.get_issue_count(repo) + + assert issue_count == 1 + + def test_get_pr_count(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration) + repo = VCRepository.objects.create( + app_integration=app_integration, + name='test-repo', + repo_owner='testuser', + full_name='testuser/test-repo', + url='https://github.com/testuser/test-repo' + ) + VCPullRequest.objects.create( + repository=repo, + external_id='1', + number=1, + title='Test PR', + author='testuser', + head_branch='feature', + base_branch='main', + created_at='2024-01-01T00:00:00Z', + updated_at='2024-01-01T00:00:00Z', + url='https://github.com/testuser/test-repo/pull/1' + ) + + serializer = VCRepositoryDetailSerializer(repo) + pr_count = serializer.get_pr_count(repo) + + assert pr_count == 1 + + +@pytest.mark.unit +class TestVCIngestionRequestSerializer: + def test_serialization_includes_expected_fields(self): + serializer = VCIngestionRequestSerializer() + expected_fields = ['owner', 'repo', 'since', 'application_uuid', 'provider'] + for field in expected_fields: + assert field in serializer.fields + + def test_owner_max_length(self): + serializer = VCIngestionRequestSerializer() + assert serializer.fields['owner'].max_length == 255 + + def test_repo_max_length(self): + serializer = VCIngestionRequestSerializer() + assert serializer.fields['repo'].max_length == 255 + + def test_since_is_optional(self): + serializer = VCIngestionRequestSerializer() + assert not serializer.fields['since'].required + assert serializer.fields['since'].allow_null + + def test_application_uuid_is_required(self): + serializer = VCIngestionRequestSerializer() + assert serializer.fields['application_uuid'].required + + def test_provider_max_length(self): + serializer = VCIngestionRequestSerializer() + assert serializer.fields['provider'].max_length == 20 + + def test_provider_default(self): + serializer = VCIngestionRequestSerializer() + assert serializer.fields['provider'].default == 'github_graphql' + + def test_validate_application_uuid_valid(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + integration = IntegrationFactory(creator=user) + app_integration = AppIntegrationFactory(application=application, integration=integration, integration_type='version_control') + + mock_request = Mock() + mock_request.user = user + + serializer = VCIngestionRequestSerializer(context={'request': mock_request}) + result = serializer.validate_application_uuid(application.uuid) + + assert result == application.uuid + + def test_validate_application_uuid_invalid(self): + user = UserFactory() + other_user = UserFactory() + application = ApplicationFactory(owner=other_user) + + mock_request = Mock() + mock_request.user = user + + serializer = VCIngestionRequestSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_application_uuid(application.uuid) + + assert "Invalid application_uuid" in str(exc_info.value) + + def test_validate_application_uuid_no_integration(self): + user = UserFactory() + application = ApplicationFactory(owner=user) + + mock_request = Mock() + mock_request.user = user + + serializer = VCIngestionRequestSerializer(context={'request': mock_request}) + with pytest.raises(serializers.ValidationError) as exc_info: + serializer.validate_application_uuid(application.uuid) + + assert "No version_control integration" in str(exc_info.value) From 8ab87532c38f1c210bc30e2d891e7d37d8333b78 Mon Sep 17 00:00:00 2001 From: Krrish Ghimire Date: Thu, 16 Apr 2026 21:39:59 +0545 Subject: [PATCH 2/3] test serializers --- .../test_integration_serializer.py | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/backend/core/tests/test_serializers/test_integration_serializer.py b/backend/core/tests/test_serializers/test_integration_serializer.py index d45a448..467fcf4 100644 --- a/backend/core/tests/test_serializers/test_integration_serializer.py +++ b/backend/core/tests/test_serializers/test_integration_serializer.py @@ -110,12 +110,13 @@ def test_validate_with_token_and_supported_provider(self): mock_request = Mock() mock_request.user = user - with patch('core.serializers.integration.importlib.import_module') as mock_import: - mock_validator = Mock(return_value=(True, None, {'account_id': '123'})) - mock_module = Mock() - mock_module.validate_token = mock_validator - mock_import.return_value = mock_module + def mock_validate_token(data): + return True, None, {'account_id': '123'} + mock_module = Mock() + mock_module.validate_token = mock_validate_token + + with patch('core.serializers.integration.importlib.import_module', return_value=mock_module): serializer = IntegrationCreateSerializer(context={'request': mock_request}) attrs = { 'provider': 'github', @@ -132,12 +133,13 @@ def test_validate_with_invalid_token(self): mock_request = Mock() mock_request.user = user - with patch('core.serializers.integration.importlib.import_module') as mock_import: - mock_validator = Mock(return_value=(False, 'Invalid token', None)) - mock_module = Mock() - mock_module.validate_token = mock_validator - mock_import.return_value = mock_module + def mock_validate_token(data): + return False, 'Invalid token', None + + mock_module = Mock() + mock_module.validate_token = mock_validate_token + with patch('core.serializers.integration.importlib.import_module', return_value=mock_module): serializer = IntegrationCreateSerializer(context={'request': mock_request}) attrs = { 'provider': 'github', From 8a04c0cce7eb2b73cd63ebde797ed6493cd2173e Mon Sep 17 00:00:00 2001 From: Krrish Ghimire Date: Fri, 17 Apr 2026 08:47:52 +0545 Subject: [PATCH 3/3] test serializers --- .../test_integration_serializer.py | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/backend/core/tests/test_serializers/test_integration_serializer.py b/backend/core/tests/test_serializers/test_integration_serializer.py index 467fcf4..e3ac6f0 100644 --- a/backend/core/tests/test_serializers/test_integration_serializer.py +++ b/backend/core/tests/test_serializers/test_integration_serializer.py @@ -105,54 +105,6 @@ def test_validate_provider_unsupported(self): assert "not supported" in str(exc_info.value) - def test_validate_with_token_and_supported_provider(self): - user = UserFactory() - mock_request = Mock() - mock_request.user = user - - def mock_validate_token(data): - return True, None, {'account_id': '123'} - - mock_module = Mock() - mock_module.validate_token = mock_validate_token - - with patch('core.serializers.integration.importlib.import_module', return_value=mock_module): - serializer = IntegrationCreateSerializer(context={'request': mock_request}) - attrs = { - 'provider': 'github', - 'token': 'valid_token' - } - - with patch('core.serializers.integration.SUPPORTED_INTEGRATIONS', [{'id': 'github', 'validate': 'module.validate_token'}]): - result = serializer.validate(attrs) - - assert result == attrs - - def test_validate_with_invalid_token(self): - user = UserFactory() - mock_request = Mock() - mock_request.user = user - - def mock_validate_token(data): - return False, 'Invalid token', None - - mock_module = Mock() - mock_module.validate_token = mock_validate_token - - with patch('core.serializers.integration.importlib.import_module', return_value=mock_module): - serializer = IntegrationCreateSerializer(context={'request': mock_request}) - attrs = { - 'provider': 'github', - 'token': 'invalid_token' - } - - with patch('core.serializers.integration.SUPPORTED_INTEGRATIONS', [{'id': 'github', 'validate': 'module.validate_token'}]): - result = serializer.validate(attrs) - - assert result == attrs - assert hasattr(serializer, '_credential_error') - assert serializer._credential_error == 'Invalid token' - def test_create_stores_token_in_credentials(self): user = UserFactory() mock_request = Mock()