Coverage for src/paperap/tests/unittest.py: 97%

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.5

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

24 from __future__ import annotations

25

26 import json

27 import logging

28 import os

29 import unittest

30 from pathlib import Path

31 from typing import TYPE_CHECKING, Any, Callable, Generic, Iterator, override

32 from unittest.mock import MagicMock, patch

33

34 from pydantic import ValidationError

35 from typing_extensions import TypeAlias, TypeVar

36

37 from paperap.client import PaperlessClient

38 from 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)

50 from 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)

59 from 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)

67

68 from paperap.tests.testcase import TestMixin

69

70 logger = logging.getLogger(__name__)

71

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

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

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

75

76 class UnitTestCase(

77 unittest.TestCase,

78 TestMixin[_StandardModel, _StandardResource, _StandardQuerySet],

79 Generic[_StandardModel, _StandardResource, _StandardQuerySet]

80):

81

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

88

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

100

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.

105

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.

109

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 )

135

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.

145

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)

153

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)

161

162 if callback:

163 self.assertTrue(callback(model), f"Condition failed for {model}")

164

165 # Check multiple results, but avoid paging

166 if count > 5:

167 break

168

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

172

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.

183

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'])

193

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)

204

205 self.assertEqual(qs.count(), expected_count)

206

207 self.assert_queryset_callback(

208 queryset = qs,

209 expected_count = expected_count,

210 callback = callback

211 )

212

213 class 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

221

222 class 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

230

231 class 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

239

240 class 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

248

249 class 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

257

258 class 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

266

267 class 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

275

276 class 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

284

285 class 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

293

294 class 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

302

303 class 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

311

312 class 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

320

321 class 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

329

330 class 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

338

339 class 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

347

348 class 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