Coverage for src/paperap/tests/testcase.py: 78%

134 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-15 03:55 -0400

1""" 

2 

3 

4 

5 ---------------------------------------------------------------------------- 

6 

7 METADATA: 

8 

9 File: testcase.py 

10 Project: paperap 

11 Created: 2025-03-04 

12 Version: 0.0.7 

13 Author: Jess Mann 

14 Email: jess@jmann.me 

15 Copyright (c) 2025 Jess Mann 

16 

17 ---------------------------------------------------------------------------- 

18 

19 LAST MODIFIED: 

20 

21 2025-03-04 By Jess Mann 

22 

23""" 

24from __future__ import annotations 

25 

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 

33 

34from pydantic import ValidationError 

35from typing_extensions import TypeAlias, TypeVar 

36 

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) 

68 

69logger = logging.getLogger(__name__) 

70 

71_StandardModel = TypeVar("_StandardModel", bound="StandardModel", default="StandardModel") 

72_StandardResource = TypeVar("_StandardResource", bound="StandardResource", default="StandardResource") 

73_StandardQuerySet = TypeVar("_StandardQuerySet", bound="StandardQuerySet", default="StandardQuerySet") 

74 

75class TestMixin(ABC, Generic[_StandardModel, _StandardResource, _StandardQuerySet]): 

76 """ 

77 A base test case class for testing Paperless NGX resources. 

78 

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://example.com', 'PAPERLESS_TOKEN': '40characterslong40characterslong40charac', 'PAPERLESS_SAVE_ON_WRITE': 'False'} 

92 

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] 

98 

99 # Instances 

100 client : "PaperlessClient" 

101 resource : _StandardResource 

102 model : _StandardModel 

103 

104 # Types (TODO only one of these should be needed) 

105 factory : type[PydanticFactory[_StandardModel]] 

106 resource_class : type[_StandardResource] 

107 model_type : type[_StandardModel] | None = None 

108 queryset_type : type[_StandardQuerySet] | None = None 

109 

110 @property 

111 def _meta(self) -> StandardModel.Meta: 

112 return self.model._meta # type: ignore # Allow private attribute access in tests 

113 

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() 

123 

124 @abstractmethod 

125 def setup_client(self, **kwargs) -> None: 

126 raise NotImplementedError("Method must be implemented in subclasses.") 

127 

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. 

132 

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. 

136 

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.") 

148 

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 

171 

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 

178 

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() 

188 

189 if not self.model_data_unparsed and (parsed := getattr(self, "model_data_parsed")): 

190 self.model_data_unparsed = self.resource.transform_data_input(**parsed) 

191 

192 def setup_model(self) -> None: 

193 """ 

194 Set up the model instance using the factory and model data. 

195 """ 

196 if getattr(self, "resource", None) and getattr(self, "model_data_unparsed", None): 

197 self.model = self.resource.parse_to_model(self.model_data_unparsed) 

198 

199 def bake_model(self, *args, **kwargs : Any) -> _StandardModel: 

200 """ 

201 Create a model instance using the factory. 

202 

203 Args: 

204 *args: Positional arguments for the factory. 

205 **kwargs: Keyword arguments for the factory. 

206 

207 Returns: 

208 A new model instance. 

209 """ 

210 return self.factory.create(*args, **kwargs) 

211 

212 def create_list(self, count : int, *args, **kwargs : Any) -> list[_StandardModel]: 

213 """ 

214 Create a list of model instances using the factory. 

215 

216 Args: 

217 count: The number of instances to create. 

218 *args: Positional arguments for the factory. 

219 **kwargs: Keyword arguments for the factory. 

220 

221 Returns: 

222 A list of new model instances. 

223 """ 

224 return [self.bake_model(*args, **kwargs) for _ in range(count)] 

225 

226 def load_model(self, resource_name : str | None = None) -> _StandardModel: 

227 """ 

228 Load a model instance from sample data. 

229 

230 Args: 

231 resource_name: The name of the resource to load data for. 

232 

233 Returns: 

234 A new model instance created from the sample data. 

235 """ 

236 sample_data = self.load_model_data(resource_name) 

237 return self.resource.parse_to_model(sample_data) 

238 

239 def load_list(self, resource_name : str | None = None) -> list[_StandardModel]: 

240 """ 

241 Load a list of model instances from sample data. 

242 

243 Args: 

244 resource_name: The name of the resource to load data for. 

245 

246 Returns: 

247 A list of new model instances created from the sample data. 

248 """ 

249 sample_data = self.load_list_data(resource_name) 

250 return [self.resource.parse_to_model(item) for item in sample_data["results"]] 

251 

252 def _call_list_resource(self, resource : type[StandardResource[_StandardModel]] | StandardResource[_StandardModel] | None = None, **kwargs : Any) -> BaseQuerySet[_StandardModel]: 

253 """ 

254 Call the list method on a resource. 

255 

256 Args: 

257 resource: The resource or resource class to call. 

258 **kwargs: Additional filter parameters. 

259 

260 Returns: 

261 A BaseQuerySet of model instances. 

262 """ 

263 if not resource: 

264 if not (resource := getattr(self,"resource", None)): 

265 raise ValueError("Resource not provided") 

266 

267 # If resource is a type, instantiate it 

268 if isinstance(resource, type): 

269 return resource(client=self.client).filter(**kwargs) 

270 return resource.filter(**kwargs) 

271 

272 def _call_get_resource(self, resource : type[StandardResource[_StandardModel]] | StandardResource[_StandardModel], pk : int) -> _StandardModel: 

273 """ 

274 Call the get method on a resource. 

275 

276 Args: 

277 resource: The resource or resource class to call. 

278 pk: The primary key of the model instance to retrieve. 

279 

280 Returns: 

281 The model instance with the specified primary key. 

282 """ 

283 # If resource is a type, instantiate it 

284 if isinstance(resource, type): 

285 return resource(client=self.client).get(pk) 

286 

287 return resource.get(pk) 

288 

289 def list_resource(self, resource : type[StandardResource[_StandardModel]] | StandardResource[_StandardModel] | None = None, **kwargs : Any) -> BaseQuerySet[_StandardModel]: 

290 """ 

291 List resources using sample data or by calling the resource. 

292 

293 Args: 

294 resource: The resource or resource class to list. 

295 **kwargs: Additional filter parameters. 

296 

297 Returns: 

298 A BaseQuerySet of model instances. 

299 """ 

300 if not resource: 

301 if not (resource := getattr(self, "resource", None)): 

302 raise ValueError("Resource not provided") 

303 

304 try: 

305 sample_data = self.load_list_data(resource.name) 

306 with patch("paperap.client.PaperlessClient.request") as request: 

307 request.return_value = sample_data 

308 qs = self._call_list_resource(resource, **kwargs) 

309 for _ in qs: 

310 pass 

311 return qs 

312 

313 except FileNotFoundError: 

314 return self._call_list_resource(resource, **kwargs) 

315 

316 def get_resource(self, resource : type[StandardResource[_StandardModel]] | StandardResource[_StandardModel], pk : int) -> _StandardModel: 

317 """ 

318 Get a resource using sample data or by calling the resource. 

319 

320 Args: 

321 resource: The resource or resource class to get. 

322 pk: The primary key of the model instance to retrieve. 

323 

324 Returns: 

325 The model instance with the specified primary key. 

326 """ 

327 try: 

328 sample_data = self.load_model_data() 

329 with patch("paperap.client.PaperlessClient.request") as request: 

330 request.return_value = sample_data 

331 return self._call_get_resource(resource, pk) 

332 except FileNotFoundError: 

333 return self._call_get_resource(resource, pk) 

334 

335 def load_model_data(self, resource_name : str | None = None) -> dict[str, Any]: 

336 """ 

337 Load model data from a sample data file. 

338 

339 Args: 

340 resource_name: The name of the resource to load data for. 

341 

342 Returns: 

343 A dictionary containing the model data. 

344 """ 

345 if not getattr(self, "model_data_parsed", None): 

346 if self.sample_data_filename: 

347 self.model_data_unparsed = load_sample_data(self.sample_data_filename) 

348 else: 

349 resource_name = resource_name or self.resource.name 

350 filename = f"{resource_name}_item.json" 

351 self.model_data_unparsed = load_sample_data(filename) 

352 

353 self.model_data_parsed = self.resource.transform_data_output(**self.model_data_unparsed) 

354 return self.model_data_parsed 

355 

356 def load_list_data(self, resource_name : str | None = None) -> dict[str, Any]: 

357 """ 

358 Load list data from a sample data file. 

359 

360 Args: 

361 resource_name: The name of the resource to load data for. 

362 

363 Returns: 

364 A dictionary containing the list data. 

365 """ 

366 if not getattr(self, "list_data", None): 

367 resource_name = resource_name or self.resource.name 

368 filename = f"{resource_name}_list.json" 

369 self.list_data = load_sample_data(filename) 

370 return self.list_data