Coverage for src/paperap/tests/unittest.py: 97%
150 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-15 03:55 -0400
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-15 03:55 -0400
1"""
5 ----------------------------------------------------------------------------
7 METADATA:
9 File: testcase.py
10 Project: paperap
11 Created: 2025-03-04
12 Version: 0.0.5
13 Author: Jess Mann
14 Email: jess@jmann.me
15 Copyright (c) 2025 Jess Mann
17 ----------------------------------------------------------------------------
19 LAST MODIFIED:
21 2025-03-04 By Jess Mann
23"""
24from __future__ import annotations
26import json
27import logging
28import os
29import unittest
30from pathlib import Path
31from typing import TYPE_CHECKING, Any, Callable, Generic, Iterator, override
32from unittest.mock import MagicMock, patch
34from pydantic import ValidationError
35from typing_extensions import TypeAlias, TypeVar
37from paperap.client import PaperlessClient
38from paperap.models import (BaseQuerySet, Correspondent, CorrespondentQuerySet,
39 CustomField, CustomFieldQuerySet,
40 Document, DocumentQuerySet, DocumentType,
41 DocumentTypeQuerySet, Group, GroupQuerySet,
42 Profile, ProfileQuerySet, SavedView,
43 SavedViewQuerySet, ShareLinks, ShareLinksQuerySet,
44 StandardModel, StandardQuerySet, StoragePath,
45 StoragePathQuerySet, Tag, TagQuerySet, Task,
46 TaskQuerySet, UISettings, UISettingsQuerySet, User,
47 UserQuerySet, Workflow, WorkflowAction,
48 WorkflowActionQuerySet, WorkflowQuerySet,
49 WorkflowTrigger, WorkflowTriggerQuerySet)
50from paperap.resources import (BaseResource, CorrespondentResource,
51 CustomFieldResource,
52 DocumentResource, DocumentTypeResource,
53 GroupResource, ProfileResource,
54 SavedViewResource, ShareLinksResource,
55 StandardResource, StoragePathResource,
56 TagResource, TaskResource, UISettingsResource,
57 UserResource, WorkflowActionResource,
58 WorkflowResource, WorkflowTriggerResource)
59from paperap.tests.factories import (CorrespondentFactory, DocumentFactory,
60 DocumentTypeFactory, GroupFactory,
61 ProfileFactory, PydanticFactory,
62 SavedViewFactory, ShareLinksFactory,
63 StoragePathFactory, TagFactory,
64 TaskFactory, UISettingsFactory,
65 UserFactory, WorkflowActionFactory,
66 WorkflowFactory, WorkflowTriggerFactory)
68from paperap.tests.testcase import TestMixin
70logger = logging.getLogger(__name__)
72_StandardModel = TypeVar("_StandardModel", bound="StandardModel", default="StandardModel")
73_StandardResource = TypeVar("_StandardResource", bound="StandardResource", default="StandardResource")
74_StandardQuerySet = TypeVar("_StandardQuerySet", bound="StandardQuerySet", default="StandardQuerySet")
76class UnitTestCase(
77 unittest.TestCase,
78 TestMixin[_StandardModel, _StandardResource, _StandardQuerySet],
79 Generic[_StandardModel, _StandardResource, _StandardQuerySet]
80):
82 @override
83 def setUp(self) -> None:
84 """
85 Set up the test case by initializing the client, resource, and model data.
86 """
87 self._reset_attributes()
89 @override
90 def setup_client(self, **kwargs) -> None:
91 """
92 Set up the PaperlessClient instance, optionally mocking environment variables.
93 """
94 if not hasattr(self, "client") or not self.client:
95 if self.mock_env:
96 with patch.dict(os.environ, self.env_data, clear=True):
97 self.client = PaperlessClient()
98 else:
99 self.client = PaperlessClient()
101 @override
102 def validate_field(self, field_name : str, test_cases : list[tuple[Any, Any]]):
103 """
104 Validate that a field is parsed correctly with various types of data.
106 Args:
107 field_name: The name of the field to test.
108 test_cases: A list of tuples with input values and expected results.
110 Examples:
111 test_cases = [
112 (42, 42),
113 ("42", 42),
114 (None, None),
115 (0, ValidationError),
116 (Decimal('42.5'), ValidationError),
117 ]
118 self.validate_field("age", test_cases)
119 """
120 self._meta.save_on_write = False
121 for (input_value, expected) in test_cases:
122 with self.subTest(field=field_name, input_value=input_value):
123 if type(expected) is type and issubclass(expected, Exception):
124 with self.assertRaises(expected, msg=f"Setting {self.model.__class__.__name__} field {field_name} failed with input {input_value}"):
125 setattr(self.model, field_name, input_value)
126 else:
127 setattr(self.model, field_name, input_value)
128 real_value = getattr(self.model, field_name)
129 self.assertIsInstance(real_value, type(expected), f"Setting {self.model.__class__.__name__} field {field_name} failed with input {input_value}, expected {expected}")
130 self.assertEqual(
131 real_value,
132 expected,
133 f"Setting {self.model.__class__.__name__} field {field_name} failed with input {input_value}"
134 )
136 def assert_queryset_callback(
137 self,
138 *,
139 queryset : StandardQuerySet[_StandardModel],
140 callback : Callable[[_StandardModel], bool] | None = None,
141 expected_count : int | None = None
142 ) -> None:
143 """
144 Generic method to test queryset filtering.
146 Args:
147 queryset: The queryset to test
148 callback: A callback function to test each model instance.
149 expected_count: The expected result count of the queryset.
150 """
151 if expected_count is not None:
152 self.assertEqual(queryset.count(), expected_count)
154 count = 0
155 for model in queryset:
156 count += 1
157 if self.model_type:
158 self.assertIsInstance(model, self.model_type)
159 else:
160 self.assertIsInstance(model, StandardModel)
162 if callback:
163 self.assertTrue(callback(model), f"Condition failed for {model}")
165 # Check multiple results, but avoid paging
166 if count > 5:
167 break
169 if expected_count is not None:
170 expected_iterations = min(expected_count, 6)
171 self.assertEqual(count, expected_iterations, f"Documents iteration unexpected. Count: {expected_count} -> Expected {expected_iterations} iterations, got {count}.")
173 def assert_queryset_callback_patched(
174 self,
175 *,
176 queryset : StandardQuerySet[_StandardModel] | Callable[..., StandardQuerySet[_StandardModel]],
177 sample_data : dict[str, Any],
178 callback : Callable[[_StandardModel], bool] | None = None,
179 expected_count : int | None = None,
180 ) -> None:
181 """
182 Generic method to test queryset filtering.
184 Args:
185 queryset: The queryset to test, or a method which retrieves a queryset.
186 sample_data: The sample data to use for the queryset.
187 callback: A callback function to test each model instance.
188 expected_count: The expected result count of the queryset.
189 """
190 # Setup defaults
191 if expected_count is None:
192 expected_count = int(sample_data['count'])
194 with patch('paperap.client.PaperlessClient.request') as mock_request:
195 mock_request.return_value = sample_data
196 if not isinstance(queryset, Callable):
197 qs = queryset
198 else:
199 qs = queryset()
200 if self.queryset_type:
201 self.assertIsInstance(qs, self.queryset_type)
202 else:
203 self.assertIsInstance(qs, BaseQuerySet)
205 self.assertEqual(qs.count(), expected_count)
207 self.assert_queryset_callback(
208 queryset = qs,
209 expected_count = expected_count,
210 callback = callback
211 )
213class CustomFieldUnitTest(UnitTestCase["CustomField", "CustomFieldResource", "CustomFieldQuerySet"]):
214 """
215 A test case for the CustomField model and resource.
216 """
217 resource_class = CustomFieldResource
218 model_type = CustomField
219 queryset_type = CustomFieldQuerySet
220 #factory = PydanticFactory
222class DocumentUnitTest(UnitTestCase["Document", "DocumentResource", "DocumentQuerySet"]):
223 """
224 A test case for the Document model and resource.
225 """
226 resource_class = DocumentResource
227 model_type = Document
228 queryset_type = DocumentQuerySet
229 factory = DocumentFactory
231class DocumentTypeUnitTest(UnitTestCase["DocumentType", "DocumentTypeResource", "DocumentTypeQuerySet"]):
232 """
233 A test case for the DocumentType model and resource.
234 """
235 resource_class = DocumentTypeResource
236 model_type = DocumentType
237 queryset_type = DocumentTypeQuerySet
238 factory = DocumentTypeFactory
240class CorrespondentUnitTest(UnitTestCase["Correspondent", "CorrespondentResource", "CorrespondentQuerySet"]):
241 """
242 A test case for the Correspondent model and resource.
243 """
244 resource_class = CorrespondentResource
245 model_type = Correspondent
246 queryset_type = CorrespondentQuerySet
247 factory = CorrespondentFactory
249class TagUnitTest(UnitTestCase["Tag", "TagResource", "TagQuerySet"]):
250 """
251 A test case for the Tag model and resource.
252 """
253 resource_class = TagResource
254 model_type = Tag
255 queryset_type = TagQuerySet
256 factory = TagFactory
258class UserUnitTest(UnitTestCase["User", "UserResource", "UserQuerySet"]):
259 """
260 A test case for the User model and resource.
261 """
262 resource_class = UserResource
263 model_type = User
264 queryset_type = UserQuerySet
265 factory = UserFactory
267class GroupUnitTest(UnitTestCase["Group", "GroupResource", "GroupQuerySet"]):
268 """
269 A test case for the Group model and resource.
270 """
271 resource_class = GroupResource
272 model_type = Group
273 queryset_type = GroupQuerySet
274 factory = GroupFactory
276class ProfileUnitTest(UnitTestCase["Profile", "ProfileResource", "ProfileQuerySet"]):
277 """
278 A test case for the Profile model and resource.
279 """
280 resource_class = ProfileResource
281 model_type = Profile
282 queryset_type = ProfileQuerySet
283 factory = ProfileFactory
285class TaskUnitTest(UnitTestCase["Task", "TaskResource", "TaskQuerySet"]):
286 """
287 A test case for the Task model and resource.
288 """
289 resource_class = TaskResource
290 model_type = Task
291 queryset_type = TaskQuerySet
292 factory = TaskFactory
294class WorkflowUnitTest(UnitTestCase["Workflow", "WorkflowResource", "WorkflowQuerySet"]):
295 """
296 A test case for the Workflow model and resource.
297 """
298 resource_class = WorkflowResource
299 model_type = Workflow
300 queryset_type = WorkflowQuerySet
301 factory = WorkflowFactory
303class SavedViewUnitTest(UnitTestCase["SavedView", "SavedViewResource", "SavedViewQuerySet"]):
304 """
305 A test case for the SavedView model and resource.
306 """
307 resource_class = SavedViewResource
308 model_type = SavedView
309 queryset_type = SavedViewQuerySet
310 factory = SavedViewFactory
312class ShareLinksUnitTest(UnitTestCase["ShareLinks", "ShareLinksResource", "ShareLinksQuerySet"]):
313 """
314 A test case for ShareLinks
315 """
316 resource_class = ShareLinksResource
317 model_type = ShareLinks
318 queryset_type = ShareLinksQuerySet
319 factory = ShareLinksFactory
321class UISettingsUnitTest(UnitTestCase["UISettings", "UISettingsResource", "UISettingsQuerySet"]):
322 """
323 A test case for the UISettings model and resource.
324 """
325 resource_class = UISettingsResource
326 model_type = UISettings
327 queryset_type = UISettingsQuerySet
328 factory = UISettingsFactory
330class StoragePathUnitTest(UnitTestCase["StoragePath", "StoragePathResource", "StoragePathQuerySet"]):
331 """
332 A test case for the StoragePath model and resource.
333 """
334 resource_class = StoragePathResource
335 model_type = StoragePath
336 queryset_type = StoragePathQuerySet
337 factory = StoragePathFactory
339class WorkflowActionUnitTest(UnitTestCase["WorkflowAction", "WorkflowActionResource", "WorkflowActionQuerySet"]):
340 """
341 A test case for the WorkflowAction model and resource.
342 """
343 resource_class = WorkflowActionResource
344 model_type = WorkflowAction
345 queryset_type = WorkflowActionQuerySet
346 factory = WorkflowActionFactory
348class WorkflowTriggerUnitTest(UnitTestCase["WorkflowTrigger", "WorkflowTriggerResource", "WorkflowTriggerQuerySet"]):
349 """
350 A test case for the WorkflowTrigger model and resource.
351 """
352 resource_class = WorkflowTriggerResource
353 model_type = WorkflowTrigger
354 queryset_type = WorkflowTriggerQuerySet
355 factory = WorkflowTriggerFactory