Coverage for src/paperap/tests/testcase.py: 78%
132 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 23:40 -0400
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 23:40 -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
26from abc import ABC, abstractmethod
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.utils import load_sample_data
60from paperap.tests.factories import (CorrespondentFactory, DocumentFactory,
61 DocumentTypeFactory, GroupFactory,
62 ProfileFactory, PydanticFactory,
63 SavedViewFactory, ShareLinksFactory,
64 StoragePathFactory, TagFactory,
65 TaskFactory, UISettingsFactory,
66 UserFactory, WorkflowActionFactory,
67 WorkflowFactory, WorkflowTriggerFactory)
69logger = logging.getLogger(__name__)
71_StandardModel = TypeVar("_StandardModel", bound="StandardModel", default="StandardModel")
72_StandardResource = TypeVar("_StandardResource", bound="StandardResource", default="StandardResource")
73_StandardQuerySet = TypeVar("_StandardQuerySet", bound="StandardQuerySet", default="StandardQuerySet")
75class TestMixin(ABC, Generic[_StandardModel, _StandardResource, _StandardQuerySet]):
76 """
77 A base test case class for testing Paperless NGX resources.
79 Attributes:
80 client: The PaperlessClient instance.
81 mock_env: Whether to mock the environment variables.
82 env_data: The environment data to use when mocking.
83 resource: The resource being tested.
84 resource_class: The class of the resource being tested.
85 factory: The factory class for creating model instances.
86 model_data_parsed: The data for creating a model instance.
87 list_data: The data for creating a list of model instances.
88 """
89 # Patching stuff
90 mock_env : bool = True
91 env_data : dict[str, Any] = {'PAPERLESS_BASE_URL': 'http://localhost:8000', 'PAPERLESS_TOKEN': 'abc123', 'PAPERLESS_SAVE_ON_WRITE': 'False'}
93 # Data for the test
94 sample_data_filename : str | None = None
95 model_data_unparsed : dict[str, Any]
96 model_data_parsed : dict[str, Any]
97 list_data : dict[str, Any]
99 # Instances
100 client : "PaperlessClient"
101 resource : _StandardResource
102 model : _StandardModel
104 # Types (TODO only one of these should be needed)
105 factory : type[PydanticFactory]
106 resource_class : type[_StandardResource]
107 model_type : type[_StandardModel] | None = None
108 queryset_type : type[_StandardQuerySet] | None = None
110 @property
111 def _meta(self) -> StandardModel.Meta:
112 return self.model._meta # type: ignore # Allow private attribute access in tests
114 def _reset_attributes(self) -> None:
115 """
116 Set up the test case by initializing the client, resource, and model data.
117 """
118 self.setup_references()
119 self.setup_client()
120 self.setup_resource()
121 self.setup_model_data()
122 self.setup_model()
124 @abstractmethod
125 def setup_client(self, **kwargs) -> None:
126 raise NotImplementedError("Method must be implemented in subclasses.")
128 @abstractmethod
129 def validate_field(self, field_name : str, test_cases : list[tuple[Any, Any]]):
130 """
131 Validate that a field is parsed correctly with various types of data.
133 Args:
134 field_name: The name of the field to test.
135 test_cases: A list of tuples with input values and expected results.
137 Examples:
138 test_cases = [
139 (42, 42),
140 ("42", 42),
141 (None, None),
142 (0, ValidationError),
143 (Decimal('42.5'), ValidationError),
144 ]
145 self.validate_field("age", test_cases)
146 """
147 raise NotImplementedError("Method must be implemented in subclasses.")
149 def setup_references(self) -> None:
150 # Check if we have each attrib, and set all the others we can
151 if hasattr(self, "modal_type"):
152 self.resource = getattr(self, "resource", self.model_type._meta.resource) # type: ignore
153 self.resource_class = getattr(self, "resource_class", self.resource.__class__) # type: ignore
154 self.queryset_type = getattr(self, "queryset_type", self.model_type._meta.queryset) # type: ignore
155 if hasattr(self, "model"):
156 self.model_type = getattr(self, "model_type", self.model.__class__) # type: ignore
157 self.resource = getattr(self, "resource", self._meta.resource) # type: ignore
158 self.resource_class = getattr(self, "resource_class", self.resource.__class__) # type: ignore
159 self.queryset_type = getattr(self, "queryset_type", self._meta.queryset) # type: ignore
160 '''
161 if hasattr(self, "factory"):
162 self.model_type = getattr(self, "model_type", self.factory._meta.model) # type: ignore
163 self.resource = getattr(self, "resource", self.model_type._meta.resource) # type: ignore
164 self.resource_class = getattr(self, "resource_class", self.resource.__class__) # type: ignore
165 self.queryset_type = getattr(self, "queryset_type", self.model_type._meta.queryset) # type: ignore
166 '''
167 if hasattr(self, "resource"):
168 self.resource_class = getattr(self, "resource_class", self.resource.__class__) # type: ignore
169 self.model_type = getattr(self, "model_type", self.resource.model_class) # type: ignore
170 self.queryset_type = getattr(self, "queryset_type", self.model_type._meta.queryset) # type: ignore
172 def setup_resource(self) -> None:
173 """
174 Set up the resource instance using the resource class.
175 """
176 if not getattr(self, "resource", None) and (resource_class := getattr(self, 'resource_class', None)):
177 self.resource = resource_class(client=self.client) # pylint: disable=not-callable
179 def setup_model_data(self) -> None:
180 """
181 Load model data if the resource is set.
182 """
183 if getattr(self, "resource", None):
184 if unparsed := getattr(self, 'model_data_unparsed', None):
185 self.model_data_parsed = self.model_data_parsed or self.resource.transform_data_output(**unparsed)
186 else:
187 self.load_model_data()
189 def setup_model(self) -> None:
190 """
191 Set up the model instance using the factory and model data.
192 """
193 if getattr(self, "resource", None) and getattr(self, "model_data_unparsed", None):
194 self.model = self.resource.parse_to_model(self.model_data_unparsed)
196 def bake_model(self, *args, **kwargs : Any) -> _StandardModel:
197 """
198 Create a model instance using the factory.
200 Args:
201 *args: Positional arguments for the factory.
202 **kwargs: Keyword arguments for the factory.
204 Returns:
205 A new model instance.
206 """
207 return self.factory.build(*args, **kwargs)
209 def create_list(self, count : int, *args, **kwargs : Any) -> list[_StandardModel]:
210 """
211 Create a list of model instances using the factory.
213 Args:
214 count: The number of instances to create.
215 *args: Positional arguments for the factory.
216 **kwargs: Keyword arguments for the factory.
218 Returns:
219 A list of new model instances.
220 """
221 return [self.bake_model(*args, **kwargs) for _ in range(count)]
223 def load_model(self, resource_name : str | None = None) -> _StandardModel:
224 """
225 Load a model instance from sample data.
227 Args:
228 resource_name: The name of the resource to load data for.
230 Returns:
231 A new model instance created from the sample data.
232 """
233 sample_data = self.load_model_data(resource_name)
234 return self.resource.parse_to_model(sample_data)
236 def load_list(self, resource_name : str | None = None) -> list[_StandardModel]:
237 """
238 Load a list of model instances from sample data.
240 Args:
241 resource_name: The name of the resource to load data for.
243 Returns:
244 A list of new model instances created from the sample data.
245 """
246 sample_data = self.load_list_data(resource_name)
247 return [self.resource.parse_to_model(item) for item in sample_data["results"]]
249 def _call_list_resource(self, resource : type[StandardResource[_StandardModel]] | StandardResource[_StandardModel] | None = None, **kwargs : Any) -> BaseQuerySet[_StandardModel]:
250 """
251 Call the list method on a resource.
253 Args:
254 resource: The resource or resource class to call.
255 **kwargs: Additional filter parameters.
257 Returns:
258 A BaseQuerySet of model instances.
259 """
260 if not resource:
261 if not (resource := getattr(self,"resource", None)):
262 raise ValueError("Resource not provided")
264 # If resource is a type, instantiate it
265 if isinstance(resource, type):
266 return resource(client=self.client).filter(**kwargs)
267 return resource.filter(**kwargs)
269 def _call_get_resource(self, resource : type[StandardResource[_StandardModel]] | StandardResource[_StandardModel], pk : int) -> _StandardModel:
270 """
271 Call the get method on a resource.
273 Args:
274 resource: The resource or resource class to call.
275 pk: The primary key of the model instance to retrieve.
277 Returns:
278 The model instance with the specified primary key.
279 """
280 # If resource is a type, instantiate it
281 if isinstance(resource, type):
282 return resource(client=self.client).get(pk)
284 return resource.get(pk)
286 def list_resource(self, resource : type[StandardResource[_StandardModel]] | StandardResource[_StandardModel] | None = None, **kwargs : Any) -> BaseQuerySet[_StandardModel]:
287 """
288 List resources using sample data or by calling the resource.
290 Args:
291 resource: The resource or resource class to list.
292 **kwargs: Additional filter parameters.
294 Returns:
295 A BaseQuerySet of model instances.
296 """
297 if not resource:
298 if not (resource := getattr(self, "resource", None)):
299 raise ValueError("Resource not provided")
301 try:
302 sample_data = self.load_list_data(resource.name)
303 with patch("paperap.client.PaperlessClient.request") as request:
304 request.return_value = sample_data
305 qs = self._call_list_resource(resource, **kwargs)
306 for _ in qs:
307 pass
308 return qs
310 except FileNotFoundError:
311 return self._call_list_resource(resource, **kwargs)
313 def get_resource(self, resource : type[StandardResource[_StandardModel]] | StandardResource[_StandardModel], pk : int) -> _StandardModel:
314 """
315 Get a resource using sample data or by calling the resource.
317 Args:
318 resource: The resource or resource class to get.
319 pk: The primary key of the model instance to retrieve.
321 Returns:
322 The model instance with the specified primary key.
323 """
324 try:
325 sample_data = self.load_model_data()
326 with patch("paperap.client.PaperlessClient.request") as request:
327 request.return_value = sample_data
328 return self._call_get_resource(resource, pk)
329 except FileNotFoundError:
330 return self._call_get_resource(resource, pk)
332 def load_model_data(self, resource_name : str | None = None) -> dict[str, Any]:
333 """
334 Load model data from a sample data file.
336 Args:
337 resource_name: The name of the resource to load data for.
339 Returns:
340 A dictionary containing the model data.
341 """
342 if not getattr(self, "model_data_parsed", None):
343 if self.sample_data_filename:
344 self.model_data_unparsed = load_sample_data(self.sample_data_filename)
345 else:
346 resource_name = resource_name or self.resource.name
347 filename = f"{resource_name}_item.json"
348 self.model_data_unparsed = load_sample_data(filename)
350 self.model_data_parsed = self.resource.transform_data_output(**self.model_data_unparsed)
351 return self.model_data_parsed
353 def load_list_data(self, resource_name : str | None = None) -> dict[str, Any]:
354 """
355 Load list data from a sample data file.
357 Args:
358 resource_name: The name of the resource to load data for.
360 Returns:
361 A dictionary containing the list data.
362 """
363 if not getattr(self, "list_data", None):
364 resource_name = resource_name or self.resource.name
365 filename = f"{resource_name}_list.json"
366 self.list_data = load_sample_data(filename)
367 return self.list_data