import json from pathlib import Path from unittest.mock import patch from django.contrib.auth.models import User from rest_framework import status from rest_framework.test import APITestCase from documents.tests.utils import DirectoriesMixin from paperless.models import ApplicationConfiguration from paperless.models import ColorConvertChoices class TestApiAppConfig(DirectoriesMixin, APITestCase): ENDPOINT = "/api/config/" def setUp(self) -> None: super().setUp() user = User.objects.create_superuser(username="temp_admin") self.client.force_authenticate(user=user) def test_api_get_config(self): """ GIVEN: - API request to get app config WHEN: - API is called THEN: - Existing config """ response = self.client.get(self.ENDPOINT, format="json") self.assertEqual(response.status_code, status.HTTP_200_OK) self.maxDiff = None self.assertDictEqual( response.data[0], { "id": 1, "output_type": None, "pages": None, "language": None, "mode": None, "skip_archive_file": None, "image_dpi": None, "unpaper_clean": None, "deskew": None, "rotate_pages": None, "rotate_pages_threshold": None, "max_image_pixels": None, "color_conversion_strategy": None, "user_args": None, "app_title": None, "app_logo": None, "barcodes_enabled": None, "barcode_enable_tiff_support": None, "barcode_string": None, "barcode_retain_split_pages": None, "barcode_enable_asn": None, "barcode_asn_prefix": None, "barcode_upscale": None, "barcode_dpi": None, "barcode_max_pages": None, "barcode_enable_tag": None, "barcode_tag_mapping": None, "ai_enabled": False, "llm_embedding_backend": None, "llm_embedding_model": None, "llm_backend": None, "llm_model": None, "llm_api_key": None, "llm_url": None, }, ) def test_api_get_ui_settings_with_config(self): """ GIVEN: - Existing config with app_title, app_logo specified WHEN: - API to retrieve uisettings is called THEN: - app_title and app_logo are included """ config = ApplicationConfiguration.objects.first() config.app_title = "Fancy New Title" config.app_logo = "/logo/example.jpg" config.save() response = self.client.get("/api/ui_settings/", format="json") self.assertDictEqual( response.data["settings"], { "app_title": config.app_title, "app_logo": config.app_logo, } | response.data["settings"], ) def test_api_update_config(self): """ GIVEN: - API request to update app config WHEN: - API is called THEN: - Correct HTTP response - Config is updated """ response = self.client.patch( f"{self.ENDPOINT}1/", json.dumps( { "color_conversion_strategy": ColorConvertChoices.RGB, }, ), content_type="application/json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) config = ApplicationConfiguration.objects.first() self.assertEqual(config.color_conversion_strategy, ColorConvertChoices.RGB) def test_api_update_config_empty_fields(self): """ GIVEN: - API request to update app config with empty string for user_args JSONField and language field WHEN: - API is called THEN: - Correct HTTP response - user_args is set to None """ response = self.client.patch( f"{self.ENDPOINT}1/", json.dumps( { "user_args": "", "language": "", "barcode_tag_mapping": "", }, ), content_type="application/json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) config = ApplicationConfiguration.objects.first() self.assertEqual(config.user_args, None) self.assertEqual(config.language, None) self.assertEqual(config.barcode_tag_mapping, None) def test_api_replace_app_logo(self): """ GIVEN: - Existing config with app_logo specified WHEN: - API to replace app_logo is called THEN: - old app_logo file is deleted """ with (Path(__file__).parent / "samples" / "simple.jpg").open("rb") as f: self.client.patch( f"{self.ENDPOINT}1/", { "app_logo": f, }, ) config = ApplicationConfiguration.objects.first() old_logo = config.app_logo self.assertTrue(Path(old_logo.path).exists()) with (Path(__file__).parent / "samples" / "simple.png").open("rb") as f: self.client.patch( f"{self.ENDPOINT}1/", { "app_logo": f, }, ) self.assertFalse(Path(old_logo.path).exists()) def test_update_llm_api_key(self): """ GIVEN: - Existing config with llm_api_key specified WHEN: - API to update llm_api_key is called with all *s - API to update llm_api_key is called with empty string THEN: - llm_api_key is unchanged - llm_api_key is set to None """ config = ApplicationConfiguration.objects.first() config.llm_api_key = "1234567890" config.save() # Test with all * response = self.client.patch( f"{self.ENDPOINT}1/", json.dumps( { "llm_api_key": "*" * 32, }, ), content_type="application/json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) config.refresh_from_db() self.assertEqual(config.llm_api_key, "1234567890") # Test with empty string response = self.client.patch( f"{self.ENDPOINT}1/", json.dumps( { "llm_api_key": "", }, ), content_type="application/json", ) self.assertEqual(response.status_code, status.HTTP_200_OK) config.refresh_from_db() self.assertEqual(config.llm_api_key, None) def test_enable_ai_index_triggers_update(self): """ GIVEN: - Existing config with AI disabled WHEN: - Config is updated to enable AI with llm_embedding_backend THEN: - LLM index is triggered to update """ config = ApplicationConfiguration.objects.first() config.ai_enabled = False config.llm_embedding_backend = None config.save() with ( patch("documents.tasks.llmindex_index.delay") as mock_update, patch("paperless_ai.indexing.vector_store_file_exists") as mock_exists, ): mock_exists.return_value = False self.client.patch( f"{self.ENDPOINT}1/", json.dumps( { "ai_enabled": True, "llm_embedding_backend": "openai", }, ), content_type="application/json", ) mock_update.assert_called_once()