kinto 23.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. kinto/__init__.py +92 -0
  2. kinto/__main__.py +249 -0
  3. kinto/authorization.py +134 -0
  4. kinto/config/__init__.py +94 -0
  5. kinto/config/kinto.tpl +270 -0
  6. kinto/contribute.json +27 -0
  7. kinto/core/__init__.py +246 -0
  8. kinto/core/authentication.py +48 -0
  9. kinto/core/authorization.py +311 -0
  10. kinto/core/cache/__init__.py +131 -0
  11. kinto/core/cache/memcached.py +112 -0
  12. kinto/core/cache/memory.py +104 -0
  13. kinto/core/cache/postgresql/__init__.py +178 -0
  14. kinto/core/cache/postgresql/schema.sql +23 -0
  15. kinto/core/cache/testing.py +208 -0
  16. kinto/core/cornice/__init__.py +93 -0
  17. kinto/core/cornice/cors.py +144 -0
  18. kinto/core/cornice/errors.py +40 -0
  19. kinto/core/cornice/pyramidhook.py +373 -0
  20. kinto/core/cornice/renderer.py +89 -0
  21. kinto/core/cornice/resource.py +205 -0
  22. kinto/core/cornice/service.py +641 -0
  23. kinto/core/cornice/util.py +138 -0
  24. kinto/core/cornice/validators/__init__.py +94 -0
  25. kinto/core/cornice/validators/_colander.py +142 -0
  26. kinto/core/cornice/validators/_marshmallow.py +182 -0
  27. kinto/core/cornice_swagger/__init__.py +92 -0
  28. kinto/core/cornice_swagger/converters/__init__.py +21 -0
  29. kinto/core/cornice_swagger/converters/exceptions.py +6 -0
  30. kinto/core/cornice_swagger/converters/parameters.py +90 -0
  31. kinto/core/cornice_swagger/converters/schema.py +249 -0
  32. kinto/core/cornice_swagger/swagger.py +725 -0
  33. kinto/core/cornice_swagger/templates/index.html +73 -0
  34. kinto/core/cornice_swagger/templates/index_script_template.html +21 -0
  35. kinto/core/cornice_swagger/util.py +42 -0
  36. kinto/core/cornice_swagger/views.py +78 -0
  37. kinto/core/decorators.py +74 -0
  38. kinto/core/errors.py +216 -0
  39. kinto/core/events.py +301 -0
  40. kinto/core/initialization.py +738 -0
  41. kinto/core/listeners/__init__.py +9 -0
  42. kinto/core/metrics.py +94 -0
  43. kinto/core/openapi.py +115 -0
  44. kinto/core/permission/__init__.py +202 -0
  45. kinto/core/permission/memory.py +167 -0
  46. kinto/core/permission/postgresql/__init__.py +489 -0
  47. kinto/core/permission/postgresql/migrations/migration_001_002.sql +18 -0
  48. kinto/core/permission/postgresql/schema.sql +41 -0
  49. kinto/core/permission/testing.py +487 -0
  50. kinto/core/resource/__init__.py +1311 -0
  51. kinto/core/resource/model.py +412 -0
  52. kinto/core/resource/schema.py +502 -0
  53. kinto/core/resource/viewset.py +230 -0
  54. kinto/core/schema.py +119 -0
  55. kinto/core/scripts.py +50 -0
  56. kinto/core/statsd.py +1 -0
  57. kinto/core/storage/__init__.py +436 -0
  58. kinto/core/storage/exceptions.py +53 -0
  59. kinto/core/storage/generators.py +58 -0
  60. kinto/core/storage/memory.py +651 -0
  61. kinto/core/storage/postgresql/__init__.py +1131 -0
  62. kinto/core/storage/postgresql/client.py +120 -0
  63. kinto/core/storage/postgresql/migrations/migration_001_002.sql +10 -0
  64. kinto/core/storage/postgresql/migrations/migration_002_003.sql +33 -0
  65. kinto/core/storage/postgresql/migrations/migration_003_004.sql +18 -0
  66. kinto/core/storage/postgresql/migrations/migration_004_005.sql +20 -0
  67. kinto/core/storage/postgresql/migrations/migration_005_006.sql +11 -0
  68. kinto/core/storage/postgresql/migrations/migration_006_007.sql +74 -0
  69. kinto/core/storage/postgresql/migrations/migration_007_008.sql +66 -0
  70. kinto/core/storage/postgresql/migrations/migration_008_009.sql +41 -0
  71. kinto/core/storage/postgresql/migrations/migration_009_010.sql +98 -0
  72. kinto/core/storage/postgresql/migrations/migration_010_011.sql +14 -0
  73. kinto/core/storage/postgresql/migrations/migration_011_012.sql +9 -0
  74. kinto/core/storage/postgresql/migrations/migration_012_013.sql +71 -0
  75. kinto/core/storage/postgresql/migrations/migration_013_014.sql +14 -0
  76. kinto/core/storage/postgresql/migrations/migration_014_015.sql +95 -0
  77. kinto/core/storage/postgresql/migrations/migration_015_016.sql +4 -0
  78. kinto/core/storage/postgresql/migrations/migration_016_017.sql +81 -0
  79. kinto/core/storage/postgresql/migrations/migration_017_018.sql +25 -0
  80. kinto/core/storage/postgresql/migrations/migration_018_019.sql +8 -0
  81. kinto/core/storage/postgresql/migrations/migration_019_020.sql +7 -0
  82. kinto/core/storage/postgresql/migrations/migration_020_021.sql +68 -0
  83. kinto/core/storage/postgresql/migrations/migration_021_022.sql +62 -0
  84. kinto/core/storage/postgresql/migrations/migration_022_023.sql +5 -0
  85. kinto/core/storage/postgresql/migrations/migration_023_024.sql +6 -0
  86. kinto/core/storage/postgresql/migrations/migration_024_025.sql +6 -0
  87. kinto/core/storage/postgresql/migrator.py +98 -0
  88. kinto/core/storage/postgresql/pool.py +55 -0
  89. kinto/core/storage/postgresql/schema.sql +143 -0
  90. kinto/core/storage/testing.py +1857 -0
  91. kinto/core/storage/utils.py +37 -0
  92. kinto/core/testing.py +182 -0
  93. kinto/core/utils.py +553 -0
  94. kinto/core/views/__init__.py +0 -0
  95. kinto/core/views/batch.py +163 -0
  96. kinto/core/views/errors.py +145 -0
  97. kinto/core/views/heartbeat.py +106 -0
  98. kinto/core/views/hello.py +69 -0
  99. kinto/core/views/openapi.py +35 -0
  100. kinto/core/views/version.py +50 -0
  101. kinto/events.py +3 -0
  102. kinto/plugins/__init__.py +0 -0
  103. kinto/plugins/accounts/__init__.py +94 -0
  104. kinto/plugins/accounts/authentication.py +63 -0
  105. kinto/plugins/accounts/scripts.py +61 -0
  106. kinto/plugins/accounts/utils.py +13 -0
  107. kinto/plugins/accounts/views.py +136 -0
  108. kinto/plugins/admin/README.md +3 -0
  109. kinto/plugins/admin/VERSION +1 -0
  110. kinto/plugins/admin/__init__.py +40 -0
  111. kinto/plugins/admin/build/VERSION +1 -0
  112. kinto/plugins/admin/build/assets/index-CYFwtKtL.css +6 -0
  113. kinto/plugins/admin/build/assets/index-DJ0m93zA.js +149 -0
  114. kinto/plugins/admin/build/assets/logo-VBRiKSPX.png +0 -0
  115. kinto/plugins/admin/build/index.html +18 -0
  116. kinto/plugins/admin/public/help.html +25 -0
  117. kinto/plugins/admin/views.py +42 -0
  118. kinto/plugins/default_bucket/__init__.py +191 -0
  119. kinto/plugins/flush.py +28 -0
  120. kinto/plugins/history/__init__.py +65 -0
  121. kinto/plugins/history/listener.py +181 -0
  122. kinto/plugins/history/views.py +66 -0
  123. kinto/plugins/openid/__init__.py +131 -0
  124. kinto/plugins/openid/utils.py +14 -0
  125. kinto/plugins/openid/views.py +193 -0
  126. kinto/plugins/prometheus.py +300 -0
  127. kinto/plugins/statsd.py +85 -0
  128. kinto/schema_validation.py +135 -0
  129. kinto/views/__init__.py +34 -0
  130. kinto/views/admin.py +195 -0
  131. kinto/views/buckets.py +45 -0
  132. kinto/views/collections.py +58 -0
  133. kinto/views/contribute.py +39 -0
  134. kinto/views/groups.py +90 -0
  135. kinto/views/permissions.py +235 -0
  136. kinto/views/records.py +133 -0
  137. kinto-23.2.1.dist-info/METADATA +232 -0
  138. kinto-23.2.1.dist-info/RECORD +142 -0
  139. kinto-23.2.1.dist-info/WHEEL +5 -0
  140. kinto-23.2.1.dist-info/entry_points.txt +5 -0
  141. kinto-23.2.1.dist-info/licenses/LICENSE +13 -0
  142. kinto-23.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1857 @@
1
+ import time
2
+ from unittest import mock
3
+
4
+ from pyramid import testing
5
+
6
+ from kinto.core import utils
7
+ from kinto.core.storage import MISSING, Filter, Sort, exceptions, heartbeat
8
+ from kinto.core.testing import DummyRequest, ThreadMixin, skip_if_ci
9
+
10
+
11
+ OBJECT_ID = "472be9ec-26fe-461b-8282-9c4e4b207ab3"
12
+
13
+
14
+ class BaseTestStorage:
15
+ backend = None
16
+
17
+ settings = {}
18
+
19
+ def setUp(self):
20
+ super().setUp()
21
+ self.storage = self.backend.load_from_config(self._get_config())
22
+ self.storage.initialize_schema()
23
+ self.id_field = "id"
24
+ self.modified_field = "last_modified"
25
+ self.client_error_patcher = None
26
+
27
+ self.obj = {"foo": "bar"}
28
+ self.storage_kw = {"resource_name": "test", "parent_id": "1234"}
29
+ self.other_parent_id = "5678"
30
+ self.auth = "Basic bWF0OjE="
31
+
32
+ def _get_config(self, settings=None):
33
+ """Mock Pyramid config object."""
34
+ if settings is None:
35
+ settings = self.settings
36
+ config = testing.setUp()
37
+ config.add_settings(settings)
38
+ return config
39
+
40
+ def tearDown(self):
41
+ mock.patch.stopall()
42
+ super().tearDown()
43
+ self.storage.flush()
44
+
45
+ def create_object(self, obj=None, id_generator=None, **kwargs):
46
+ obj = obj or self.obj
47
+ kw = {**self.storage_kw, **kwargs}
48
+ return self.storage.create(obj=obj, id_generator=id_generator, **kw)
49
+
50
+ def test_raises_backend_error_if_error_occurs_on_client(self):
51
+ self.client_error_patcher.start()
52
+ self.assertRaises(exceptions.BackendError, self.storage.list_all, **self.storage_kw)
53
+ self.assertRaises(exceptions.BackendError, self.storage.count_all, **self.storage_kw)
54
+
55
+ def test_backend_error_provides_original_exception(self):
56
+ self.client_error_patcher.start()
57
+ try:
58
+ self.storage.list_all(**self.storage_kw)
59
+ except exceptions.BackendError as e:
60
+ error = e
61
+ self.assertTrue(isinstance(error.original, Exception))
62
+
63
+ def test_backend_error_is_raised_anywhere(self):
64
+ self.client_error_patcher.start()
65
+ calls = [
66
+ (self.storage.resource_timestamp, {}),
67
+ (self.storage.create, dict(obj={})),
68
+ (self.storage.get, dict(object_id={})),
69
+ (self.storage.update, dict(object_id="", obj={})),
70
+ (self.storage.delete, dict(object_id="")),
71
+ (self.storage.delete_all, {}),
72
+ (self.storage.purge_deleted, {}),
73
+ (self.storage.list_all, {}),
74
+ (self.storage.count_all, {}),
75
+ ]
76
+ for call, kwargs in calls:
77
+ kwargs.update(**self.storage_kw)
78
+ self.assertRaises(exceptions.BackendError, call, **kwargs)
79
+ self.assertRaises(exceptions.BackendError, self.storage.flush)
80
+
81
+ def test_initialize_schema_is_idempotent(self):
82
+ self.storage.initialize_schema()
83
+ self.storage.initialize_schema() # not raising.
84
+
85
+ def test_ping_returns_false_if_unavailable(self):
86
+ request = DummyRequest()
87
+ request.headers["Authorization"] = self.auth
88
+ request.registry.settings = {"readonly": "false"}
89
+ ping = heartbeat(self.storage)
90
+
91
+ with mock.patch("kinto.core.storage.random.SystemRandom.random", return_value=0.7):
92
+ ping(request)
93
+
94
+ self.client_error_patcher.start()
95
+ with mock.patch("kinto.core.storage.random.SystemRandom.random", return_value=0.7):
96
+ self.assertFalse(ping(request))
97
+ with mock.patch("kinto.core.storage.random.SystemRandom.random", return_value=0.5):
98
+ self.assertFalse(ping(request))
99
+
100
+ def test_ping_returns_true_when_working(self):
101
+ request = DummyRequest()
102
+ request.headers["Authorization"] = self.auth
103
+ ping = heartbeat(self.storage)
104
+ with mock.patch("kinto.core.storage.random.SystemRandom.random", return_value=0.7):
105
+ self.assertTrue(ping(request))
106
+ with mock.patch("kinto.core.storage.random.SystemRandom.random", return_value=0.5):
107
+ self.assertTrue(ping(request))
108
+
109
+ def test_ping_returns_true_when_working_in_readonly_mode(self):
110
+ request = DummyRequest()
111
+ request.headers["Authorization"] = self.auth
112
+ request.registry.settings = {"readonly": "true"}
113
+ ping = heartbeat(self.storage)
114
+ self.assertTrue(ping(request))
115
+
116
+ def test_ping_returns_false_if_unavailable_in_readonly_mode(self):
117
+ request = DummyRequest()
118
+ request.headers["Authorization"] = self.auth
119
+ request.registry.settings = {"readonly": "true"}
120
+ ping = heartbeat(self.storage)
121
+ with mock.patch.object(
122
+ self.storage, "list_all", side_effect=exceptions.BackendError("Boom!")
123
+ ):
124
+ self.assertFalse(ping(request))
125
+
126
+ def test_ping_logs_error_if_unavailable(self):
127
+ request = DummyRequest()
128
+ self.client_error_patcher.start()
129
+ ping = heartbeat(self.storage)
130
+
131
+ with mock.patch("kinto.core.storage.logger.exception") as exc_handler:
132
+ self.assertFalse(ping(request))
133
+
134
+ self.assertTrue(exc_handler.called)
135
+
136
+ def test_ping_leaves_no_tombstone(self):
137
+ request = DummyRequest()
138
+ request.headers["Authorization"] = self.auth
139
+ ping = heartbeat(self.storage)
140
+ with mock.patch("kinto.core.storage.random.SystemRandom.random", return_value=0.7):
141
+ ping(request)
142
+ with mock.patch("kinto.core.storage.random.SystemRandom.random", return_value=0.5):
143
+ ping(request)
144
+ objects = self.storage.list_all(
145
+ parent_id="__heartbeat__", resource_name="__heartbeat__", include_deleted=True
146
+ )
147
+ self.assertEqual(len(objects), 0)
148
+
149
+ def test_create_adds_the_object_id(self):
150
+ obj = self.create_object()
151
+ self.assertIsNotNone(obj["id"])
152
+
153
+ def test_create_works_as_expected(self):
154
+ stored = self.create_object()
155
+ retrieved = self.storage.get(object_id=stored["id"], **self.storage_kw)
156
+ self.assertEqual(retrieved, stored)
157
+
158
+ def test_create_copies_the_object_before_modifying_it(self):
159
+ self.create_object()
160
+ self.assertEqual(self.obj.get("id"), None)
161
+
162
+ def test_create_uses_the_resource_id_generator(self):
163
+ obj = self.create_object(id_generator=lambda: OBJECT_ID)
164
+ self.assertEqual(obj["id"], OBJECT_ID)
165
+
166
+ def test_create_supports_unicode_for_parent_and_id(self):
167
+ unicode_id = "Rémy"
168
+ self.create_object(parent_id=unicode_id, resource_name=unicode_id)
169
+
170
+ def test_create_does_not_overwrite_the_provided_id(self):
171
+ obj = {**self.obj, self.id_field: OBJECT_ID}
172
+ stored = self.create_object(obj=obj)
173
+ self.assertEqual(stored[self.id_field], OBJECT_ID)
174
+
175
+ def test_create_raise_unicity_error_if_provided_id_exists(self):
176
+ obj = {**self.obj, self.id_field: OBJECT_ID}
177
+ self.create_object(obj=obj)
178
+ obj = {**self.obj, self.id_field: OBJECT_ID}
179
+ self.assertRaises(exceptions.UnicityError, self.create_object, obj=obj)
180
+
181
+ def test_create_does_generate_a_new_last_modified_field(self):
182
+ obj = {**self.obj}
183
+ self.assertNotIn(self.modified_field, obj)
184
+ created = self.create_object(obj=obj)
185
+ self.assertIn(self.modified_field, created)
186
+
187
+ def test_get_raise_on_object_not_found(self):
188
+ self.assertRaises(
189
+ exceptions.ObjectNotFoundError,
190
+ self.storage.get,
191
+ object_id=OBJECT_ID,
192
+ **self.storage_kw,
193
+ )
194
+
195
+ def test_update_creates_a_new_object_when_needed(self):
196
+ self.assertRaises(
197
+ exceptions.ObjectNotFoundError,
198
+ self.storage.get,
199
+ object_id=OBJECT_ID,
200
+ **self.storage_kw,
201
+ )
202
+ obj = self.storage.update(object_id=OBJECT_ID, obj=self.obj, **self.storage_kw)
203
+ retrieved = self.storage.get(object_id=OBJECT_ID, **self.storage_kw)
204
+ self.assertEqual(retrieved, obj)
205
+
206
+ def test_update_overwrites_object_id(self):
207
+ stored = self.create_object()
208
+ object_id = stored[self.id_field]
209
+ self.obj[self.id_field] = "this-will-be-ignored"
210
+ self.storage.update(object_id=object_id, obj=self.obj, **self.storage_kw)
211
+ retrieved = self.storage.get(object_id=object_id, **self.storage_kw)
212
+ self.assertEqual(retrieved[self.id_field], object_id)
213
+
214
+ def test_update_generates_a_new_last_modified_field_if_not_present(self):
215
+ stored = self.create_object()
216
+ object_id = stored[self.id_field]
217
+ self.assertNotIn(self.modified_field, self.obj)
218
+ self.storage.update(object_id=object_id, obj=self.obj, **self.storage_kw)
219
+ retrieved = self.storage.get(object_id=object_id, **self.storage_kw)
220
+ self.assertIn(self.modified_field, retrieved)
221
+ self.assertGreater(retrieved[self.modified_field], stored[self.modified_field])
222
+
223
+ def test_delete_works_properly(self):
224
+ stored = self.create_object()
225
+ self.storage.delete(object_id=stored["id"], **self.storage_kw)
226
+ self.assertRaises( # Shouldn't exist.
227
+ exceptions.ObjectNotFoundError,
228
+ self.storage.get,
229
+ object_id=stored["id"],
230
+ **self.storage_kw,
231
+ )
232
+
233
+ def test_delete_works_even_on_second_time(self):
234
+ # Create an object
235
+ self.storage.create(resource_name="test", parent_id="1234", obj={"id": "demo"})
236
+ # Delete the object
237
+ self.storage.delete(
238
+ resource_name="test", parent_id="1234", object_id="demo", with_deleted=True
239
+ )
240
+ # Update an object (it recreates it.)
241
+ self.storage.update(
242
+ resource_name="test", parent_id="1234", object_id="demo", obj={"id": "demo"}
243
+ )
244
+ # Delete the object without errors
245
+ self.storage.delete(
246
+ resource_name="test", parent_id="1234", object_id="demo", with_deleted=True
247
+ )
248
+
249
+ def test_delete_can_specify_the_last_modified(self):
250
+ stored = self.create_object()
251
+ last_modified = stored[self.modified_field] + 10
252
+ self.storage.delete(object_id=stored["id"], last_modified=last_modified, **self.storage_kw)
253
+
254
+ objects = self.storage.list_all(include_deleted=True, **self.storage_kw)
255
+ self.assertEqual(objects[0][self.modified_field], last_modified)
256
+
257
+ def test_delete_raise_when_unknown(self):
258
+ self.assertRaises(
259
+ exceptions.ObjectNotFoundError,
260
+ self.storage.delete,
261
+ object_id=OBJECT_ID,
262
+ **self.storage_kw,
263
+ )
264
+
265
+ def test_list_all_handles_parent_id_pattern_matching(self):
266
+ self.create_object(parent_id="abc", resource_name="c")
267
+ obj = self.create_object(parent_id="abc", resource_name="c")
268
+ self.storage.delete(object_id=obj["id"], parent_id="abc", resource_name="c")
269
+ self.create_object(parent_id="efg", resource_name="c")
270
+
271
+ objects = self.storage.list_all(parent_id="ab*", resource_name="c", include_deleted=True)
272
+ self.assertEqual(len(objects), 2)
273
+ total_objects = self.storage.count_all(parent_id="ab*", resource_name="c")
274
+ self.assertEqual(total_objects, 1)
275
+
276
+ def test_list_all_does_proper_parent_id_pattern_matching(self):
277
+ self.create_object(parent_id="abc", resource_name="c")
278
+ self.create_object(parent_id="xabcx", resource_name="c")
279
+ self.create_object(parent_id="efg", resource_name="c")
280
+
281
+ objects = self.storage.list_all(parent_id="ab*", resource_name="c", include_deleted=True)
282
+ self.assertEqual(len(objects), 1)
283
+ total_objects = self.storage.count_all(parent_id="ab*", resource_name="c")
284
+ self.assertEqual(len(objects), total_objects)
285
+
286
+ def test_list_all_parent_id_handles_collisions(self):
287
+ abc1 = self.create_object(
288
+ parent_id="abc1", resource_name="c", obj={"id": "abc", "secret_data": "abc1"}
289
+ )
290
+ abc2 = self.create_object(
291
+ parent_id="abc2", resource_name="c", obj={"id": "abc", "secret_data": "abc2"}
292
+ )
293
+ objects = self.storage.list_all(parent_id="ab*", resource_name="c", include_deleted=True)
294
+ self.assertEqual(len(objects), 2)
295
+ total_objects = self.storage.count_all(parent_id="ab*", resource_name="c")
296
+ self.assertEqual(len(objects), total_objects)
297
+ objects.sort(key=lambda obj: obj["secret_data"])
298
+ self.assertEqual(objects[0], abc1)
299
+ self.assertEqual(objects[1], abc2)
300
+
301
+ def test_return_all_values(self):
302
+ for x in range(10):
303
+ obj = dict(self.obj)
304
+ obj["number"] = x
305
+ self.create_object(obj)
306
+
307
+ objects = self.storage.list_all(**self.storage_kw)
308
+ self.assertEqual(len(objects), 10)
309
+ total_objects = self.storage.count_all(**self.storage_kw)
310
+ self.assertEqual(len(objects), total_objects)
311
+
312
+ def test_list_all_handle_limit(self):
313
+ for x in range(10):
314
+ obj = dict(self.obj)
315
+ obj["number"] = x
316
+ self.create_object(obj)
317
+
318
+ objects = self.storage.list_all(include_deleted=True, limit=2, **self.storage_kw)
319
+ self.assertEqual(len(objects), 2)
320
+
321
+ def test_list_all_handle_sorting_on_id(self):
322
+ for x in range(3):
323
+ self.create_object()
324
+ sorting = [Sort("id", 1)]
325
+ objects = self.storage.list_all(sorting=sorting, **self.storage_kw)
326
+ self.assertTrue(objects[0]["id"] < objects[-1]["id"])
327
+
328
+ def test_list_all_handle_sorting_on_subobject(self):
329
+ for x in range(10):
330
+ obj = dict(**self.obj)
331
+ obj["person"] = dict(age=x)
332
+ self.create_object(obj)
333
+ sorting = [Sort("person.age", 1)]
334
+ objects = self.storage.list_all(sorting=sorting, **self.storage_kw)
335
+ self.assertLess(objects[0]["person"]["age"], objects[-1]["person"]["age"])
336
+
337
+ def test_list_all_sorting_is_consistent_with_filtering(self):
338
+ self.create_object({"flavor": "strawberry"})
339
+ self.create_object({"flavor": "blueberry", "author": None})
340
+ self.create_object({"flavor": "raspberry", "author": 1})
341
+ self.create_object({"flavor": "orange", "author": True})
342
+ self.create_object({"flavor": "watermelon", "author": "Ethan"})
343
+ sorting = [Sort("author", 1)]
344
+ objects = self.storage.list_all(sorting=sorting, **self.storage_kw)
345
+ # Some interesting values to compare against
346
+ values = ["A", "Z", "", 0, 4, MISSING]
347
+
348
+ for value in values:
349
+ # Together, these filters should provide the entire list
350
+ filter_less = Filter("author", value, utils.COMPARISON.LT)
351
+ filter_min = Filter("author", value, utils.COMPARISON.MIN)
352
+ smaller_objects = self.storage.list_all(
353
+ filters=[filter_less], sorting=sorting, **self.storage_kw
354
+ )
355
+ greater_objects = self.storage.list_all(
356
+ filters=[filter_min], sorting=sorting, **self.storage_kw
357
+ )
358
+ other_objects = smaller_objects + greater_objects
359
+ self.assertEqual(
360
+ objects,
361
+ other_objects,
362
+ "Filtering is not consistent with sorting when filtering "
363
+ "using value {}: {} (LT) + {} (MIN) != {}".format(
364
+ value, smaller_objects, greater_objects, objects
365
+ ),
366
+ )
367
+
368
+ # Same test but with MAX and GT
369
+ for value in values:
370
+ # Together, these filters should provide the entire list
371
+ filter_less = Filter("author", value, utils.COMPARISON.MAX)
372
+ filter_min = Filter("author", value, utils.COMPARISON.GT)
373
+ smaller_objects = self.storage.list_all(
374
+ filters=[filter_less], sorting=sorting, **self.storage_kw
375
+ )
376
+ greater_objects = self.storage.list_all(
377
+ filters=[filter_min], sorting=sorting, **self.storage_kw
378
+ )
379
+ other_objects = smaller_objects + greater_objects
380
+ self.assertEqual(
381
+ objects,
382
+ other_objects,
383
+ "Filtering is not consistent with sorting when filtering "
384
+ "using value {}: {} (MAX) + {} (GT) != {}".format(
385
+ value, smaller_objects, greater_objects, objects
386
+ ),
387
+ )
388
+
389
+ def test_list_all_can_filter_with_list_of_values(self):
390
+ for code in ["a", "b", "c"]:
391
+ self.create_object({"code": code})
392
+ filters = [Filter("code", ["a", "b"], utils.COMPARISON.IN)]
393
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
394
+ self.assertEqual(len(objects), 2)
395
+
396
+ def test_list_all_can_filter_on_array_that_contains_values(self):
397
+ self.create_object({"colors": ["red", "green", "blue"]})
398
+ self.create_object({"colors": ["gray", "blue"]})
399
+ self.create_object({"colors": ["red", "gray", "blue"]})
400
+ self.create_object({"colors": ["purple", "green", "blue"]})
401
+
402
+ filters = [Filter("colors", ["red"], utils.COMPARISON.CONTAINS)]
403
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
404
+ self.assertEqual(len(objects), 2)
405
+
406
+ filters = [Filter("colors", ["red", "gray"], utils.COMPARISON.CONTAINS)]
407
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
408
+ self.assertEqual(len(objects), 1)
409
+
410
+ def test_list_all_can_filter_on_field_that_do_not_contains_an_array_with_contains_any(self):
411
+ self.create_object({"colors": ["red", "green", "blue"]})
412
+ self.create_object({"colors": {"html": "#00FF00"}})
413
+
414
+ filters = [Filter("colors", ["red"], utils.COMPARISON.CONTAINS_ANY)]
415
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
416
+ self.assertEqual(len(objects), 1)
417
+
418
+ filters = [Filter("colors", [{"html": "#00FF00"}], utils.COMPARISON.CONTAINS_ANY)]
419
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
420
+ self.assertEqual(len(objects), 0)
421
+
422
+ def test_list_all_can_filter_on_array_that_contains_any_value(self):
423
+ self.create_object({"colors": ["red", "green", "blue"]})
424
+ self.create_object({"colors": ["gray", "blue"]})
425
+ self.create_object({"colors": ["red", "gray", "blue"]})
426
+ self.create_object({"colors": ["purple", "green", "blue"]})
427
+
428
+ filters = [Filter("colors", ["red"], utils.COMPARISON.CONTAINS_ANY)]
429
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
430
+ self.assertEqual(len(objects), 2)
431
+
432
+ filters = [Filter("colors", ["red", "gray"], utils.COMPARISON.CONTAINS_ANY)]
433
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
434
+ self.assertEqual(len(objects), 3)
435
+
436
+ def test_list_all_can_filter_on_array_that_contains_numeric_values(self):
437
+ self.create_object({"fib": [1, 2, 3]})
438
+ self.create_object({"fib": [2, 3, 5]})
439
+ self.create_object({"fib": [3, 5, 8]})
440
+ self.create_object({"fib": [5, 8, 13]})
441
+
442
+ filters = [Filter("fib", [2], utils.COMPARISON.CONTAINS)]
443
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
444
+ self.assertEqual(len(objects), 2)
445
+
446
+ filters = [Filter("fib", [2, 3], utils.COMPARISON.CONTAINS)]
447
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
448
+ self.assertEqual(len(objects), 2)
449
+
450
+ def test_list_all_can_filter_on_array_that_contains_any_numeric_value(self):
451
+ self.create_object({"fib": [1, 2, 3]})
452
+ self.create_object({"fib": [2, 3, 5]})
453
+ self.create_object({"fib": [3, 5, 8]})
454
+ self.create_object({"fib": [5, 8, 13]})
455
+
456
+ filters = [Filter("fib", [2], utils.COMPARISON.CONTAINS_ANY)]
457
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
458
+ self.assertEqual(len(objects), 2)
459
+
460
+ filters = [Filter("fib", [2, 3], utils.COMPARISON.CONTAINS_ANY)]
461
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
462
+ self.assertEqual(len(objects), 3)
463
+
464
+ def test_list_all_can_filter_on_array_with_contains_and_missing_field(self):
465
+ self.create_object({"code": "black"})
466
+ self.create_object({"fib": [2, 3, 5]})
467
+ self.create_object({"fib": [3, 5, 8]})
468
+ self.create_object({"fib": [5, 8, 13]})
469
+
470
+ filters = [Filter("fib", [2], utils.COMPARISON.CONTAINS)]
471
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
472
+ self.assertEqual(len(objects), 1)
473
+
474
+ def test_list_all_can_filter_on_array_with_contains_any_and_missing_field(self):
475
+ self.create_object({"code": "black"})
476
+ self.create_object({"fib": [2, 3, 5]})
477
+ self.create_object({"fib": [3, 5, 8]})
478
+ self.create_object({"fib": [5, 8, 13]})
479
+
480
+ filters = [Filter("fib", [2], utils.COMPARISON.CONTAINS_ANY)]
481
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
482
+ self.assertEqual(len(objects), 1)
483
+
484
+ def test_list_all_can_filter_on_array_with_contains_and_unsupported_type(self):
485
+ self.create_object({"code": "black"})
486
+ self.create_object({"fib": [2, 3, 5]})
487
+ self.create_object({"fib": [3, 5, 8]})
488
+ self.create_object({"fib": [5, 8, 13]})
489
+
490
+ filters = [Filter("fib", [{"demo": "foobar"}], utils.COMPARISON.CONTAINS)]
491
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
492
+ self.assertEqual(len(objects), 0)
493
+
494
+ def test_list_all_contains_ignores_object_if_field_is_not_array(self):
495
+ self.create_object({"code": "black"})
496
+ self.create_object({"fib": ["a", "b", "c"]})
497
+
498
+ filters = [Filter("fib", ["a"], utils.COMPARISON.CONTAINS)]
499
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
500
+ self.assertEqual(len(objects), 1)
501
+
502
+ def test_list_all_can_filter_on_array_with_contains_any_and_unsupported_type(self):
503
+ self.create_object({"code": "black"})
504
+ self.create_object({"fib": [2, 3, 5]})
505
+ self.create_object({"fib": [3, 5, 8]})
506
+ self.create_object({"fib": [5, 8, 13]})
507
+
508
+ filters = [Filter("fib", [{"demo": "foobar"}], utils.COMPARISON.CONTAINS_ANY)]
509
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
510
+ self.assertEqual(len(objects), 0)
511
+
512
+ def test_list_all_can_filter_with_numeric_values(self):
513
+ self.create_object({"missing": "code"})
514
+ for code in [1, 10, 6, 46]:
515
+ self.create_object({"code": code})
516
+
517
+ sorting = [Sort("code", 1)]
518
+ filters = [Filter("code", 10, utils.COMPARISON.MAX)]
519
+ objects = self.storage.list_all(sorting=sorting, filters=filters, **self.storage_kw)
520
+ self.assertEqual(objects[0]["code"], 1)
521
+ self.assertEqual(objects[1]["code"], 6)
522
+ self.assertEqual(objects[2]["code"], 10)
523
+ self.assertEqual(len(objects), 3)
524
+
525
+ filters = [Filter("code", 10, utils.COMPARISON.LT)]
526
+ objects = self.storage.list_all(sorting=sorting, filters=filters, **self.storage_kw)
527
+ self.assertEqual(objects[0]["code"], 1)
528
+ self.assertEqual(objects[1]["code"], 6)
529
+ self.assertEqual(len(objects), 2)
530
+
531
+ def test_list_all_can_filter_with_numeric_id(self):
532
+ for code in [0, 42]:
533
+ self.create_object({"id": str(code)})
534
+
535
+ filters = [Filter("id", 0, utils.COMPARISON.EQ)]
536
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
537
+ self.assertEqual(len(objects), 1)
538
+
539
+ filters = [Filter("id", 42, utils.COMPARISON.EQ)]
540
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
541
+ self.assertEqual(len(objects), 1)
542
+
543
+ def test_list_all_can_filter_with_numeric_strings(self):
544
+ for code in ["0566199093", "0781566199"]:
545
+ self.create_object({"phone": code})
546
+ filters = [Filter("phone", "0566199093", utils.COMPARISON.EQ)]
547
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
548
+ self.assertEqual(len(objects), 1)
549
+
550
+ def test_list_all_can_filter_with_empty_numeric_strings(self):
551
+ for code in ["0566199093", "0781566199"]:
552
+ self.create_object({"phone": code})
553
+ filters = [Filter("phone", "", utils.COMPARISON.EQ)]
554
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
555
+ self.assertEqual(len(objects), 0)
556
+
557
+ def test_list_all_can_filter_with_float_values(self):
558
+ for code in [10, 11.5, 8.5, 6, 7.5]:
559
+ self.create_object({"note": code})
560
+ filters = [Filter("note", 9.5, utils.COMPARISON.LT)]
561
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
562
+ self.assertEqual(len(objects), 3)
563
+
564
+ def test_list_all_can_filter_with_strings(self):
565
+ for code in ["Rémy", "Alexis", "Marie"]:
566
+ self.create_object({"name": code})
567
+ sorting = [Sort("name", 1)]
568
+ filters = [Filter("name", "Mathieu", utils.COMPARISON.LT)]
569
+ objects = self.storage.list_all(sorting=sorting, filters=filters, **self.storage_kw)
570
+ self.assertEqual(objects[0]["name"], "Alexis")
571
+ self.assertEqual(objects[1]["name"], "Marie")
572
+ self.assertEqual(len(objects), 2)
573
+
574
+ def test_list_all_can_filter_minimum_value_with_strings(self):
575
+ for v in ["49.0", "6.0", "53.0b4"]:
576
+ self.create_object({"product": {"version": v}})
577
+ sorting = [Sort("product.version", 1)]
578
+ filters = [Filter("product.version", "50.0", utils.COMPARISON.MIN)]
579
+ objects = self.storage.list_all(sorting=sorting, filters=filters, **self.storage_kw)
580
+ self.assertEqual(objects[0]["product"]["version"], "53.0b4")
581
+ self.assertEqual(objects[1]["product"]["version"], "6.0")
582
+ self.assertEqual(len(objects), 2)
583
+
584
+ def test_list_all_does_not_implicitly_cast(self):
585
+ for v in ["49.0", "6.0", "53.0b4"]:
586
+ self.create_object({"product": {"version": v}})
587
+ sorting = [Sort("product.version", 1)]
588
+ filters = [Filter("product.version", 50.0, utils.COMPARISON.MIN)]
589
+ objects = self.storage.list_all(sorting=sorting, filters=filters, **self.storage_kw)
590
+ self.assertEqual(len(objects), 0) # 50 (number) > strings
591
+
592
+ def test_list_all_can_deal_with_none_values(self):
593
+ self.create_object({"name": "Alexis"})
594
+ self.create_object({"title": "haha"})
595
+ self.create_object({"name": "Mathieu"})
596
+ filters = [Filter("name", "Fanny", utils.COMPARISON.GT)]
597
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
598
+ # NULLs compare as greater than everything
599
+ self.assertEqual(len(objects), 2)
600
+ # But we aren't clear on what the order will be
601
+ mathieu_object = objects[0] if "name" in objects[0] else objects[1]
602
+ haha_object = objects[1] if "name" in objects[0] else objects[0]
603
+ self.assertEqual(mathieu_object["name"], "Mathieu")
604
+ self.assertEqual(haha_object["title"], "haha")
605
+
606
+ filters = [Filter("name", "Fanny", utils.COMPARISON.LT)]
607
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
608
+ self.assertEqual(len(objects), 1)
609
+ self.assertEqual(objects[0]["name"], "Alexis")
610
+
611
+ def test_list_all_can_filter_with_none_values(self):
612
+ self.create_object({"name": "Alexis", "salary": None})
613
+ self.create_object({"name": "Mathieu", "salary": "null"})
614
+ self.create_object({"name": "Niko", "salary": ""})
615
+ self.create_object({"name": "Ethan"}) # missing salary
616
+ filters = [Filter("salary", None, utils.COMPARISON.EQ)]
617
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
618
+ self.assertEqual(len(objects), 1)
619
+ self.assertEqual(objects[0]["name"], "Alexis")
620
+
621
+ def test_filter_none_values_can_be_combined(self):
622
+ self.create_object({"name": "Alexis", "salary": None})
623
+ self.create_object({"name": "Mathieu", "salary": "null"})
624
+ self.create_object({"name": "Niko", "salary": ""})
625
+ self.create_object({"name": "Ethan"}) # missing salary
626
+ filters = [
627
+ Filter("salary", 0, utils.COMPARISON.GT),
628
+ Filter("salary", True, utils.COMPARISON.HAS),
629
+ ]
630
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
631
+ self.assertEqual(len([r for r in objects if "salary" not in r]), 0)
632
+
633
+ def test_list_all_can_filter_with_list_of_values_on_id(self):
634
+ object1 = self.create_object({"code": "a"})
635
+ object2 = self.create_object({"code": "b"})
636
+ filters = [Filter("id", [object1["id"], object2["id"]], utils.COMPARISON.IN)]
637
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
638
+ self.assertEqual(len(objects), 2)
639
+
640
+ def test_list_all_returns_empty_when_including_list_of_empty_values(self):
641
+ self.create_object({"code": "a"})
642
+ self.create_object({"code": "b"})
643
+ filters = [Filter("id", [], utils.COMPARISON.IN)]
644
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
645
+ self.assertEqual(len(objects), 0)
646
+
647
+ def test_list_all_can_filter_with_list_of_excluded_values(self):
648
+ for code in ["a", "b", "c"]:
649
+ self.create_object({"code": code})
650
+ filters = [Filter("code", ("a", "b"), utils.COMPARISON.EXCLUDE)]
651
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
652
+ self.assertEqual(len(objects), 1)
653
+
654
+ def test_list_all_can_filter_a_list_of_integer_values(self):
655
+ for code in [1, 2, 3]:
656
+ self.create_object({"code": code})
657
+ filters = [Filter("code", (1, 2), utils.COMPARISON.EXCLUDE)]
658
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
659
+ self.assertEqual(len(objects), 1)
660
+
661
+ def test_list_all_can_filter_a_list_of_mixed_typed_values(self):
662
+ for code in [1, 2, 3]:
663
+ self.create_object({"code": code})
664
+ filters = [Filter("code", (1, "b"), utils.COMPARISON.EXCLUDE)]
665
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
666
+ self.assertEqual(len(objects), 2)
667
+
668
+ def test_list_all_can_filter_a_list_of_integer_values_on_subobjects(self):
669
+ for code in [1, 2, 3]:
670
+ self.create_object({"code": {"city": code}})
671
+ filters = [Filter("code.city", (1, 2), utils.COMPARISON.EXCLUDE)]
672
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
673
+ self.assertEqual(len(objects), 1)
674
+
675
+ def test_list_all_can_filter_matching_a_list(self):
676
+ self.create_object({"flavor": "strawberry", "orders": []})
677
+ self.create_object({"flavor": "blueberry", "orders": [1]})
678
+ self.create_object({"flavor": "pineapple", "orders": [1, 2]})
679
+ self.create_object({"flavor": "watermelon", "orders": ""})
680
+ self.create_object({"flavor": "raspberry", "orders": {}})
681
+ filters = [Filter("orders", [], utils.COMPARISON.EQ)]
682
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
683
+ self.assertEqual(len(objects), 1)
684
+ self.assertEqual(objects[0]["flavor"], "strawberry")
685
+
686
+ filters = [Filter("orders", [1], utils.COMPARISON.EQ)]
687
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
688
+ self.assertEqual(len(objects), 1)
689
+ self.assertEqual(objects[0]["flavor"], "blueberry")
690
+
691
+ def test_list_all_can_filter_matching_an_object(self):
692
+ self.create_object({"flavor": "strawberry", "attributes": {}})
693
+ self.create_object(
694
+ {"flavor": "blueberry", "attributes": {"ibu": 25, "seen_on": "2017-06-01"}}
695
+ )
696
+ self.create_object(
697
+ {
698
+ "flavor": "watermelon",
699
+ "attributes": {"ibu": 25, "seen_on": "2017-06-01", "price": 9.99},
700
+ }
701
+ )
702
+ self.create_object({"flavor": "raspberry", "attributes": []})
703
+ filters = [Filter("attributes", {}, utils.COMPARISON.EQ)]
704
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
705
+ self.assertEqual(len(objects), 1)
706
+ self.assertEqual(objects[0]["flavor"], "strawberry")
707
+
708
+ filters = [Filter("attributes", {"ibu": 25, "seen_on": "2017-06-01"}, utils.COMPARISON.EQ)]
709
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
710
+ self.assertEqual(len(objects), 1)
711
+ self.assertEqual(objects[0]["flavor"], "blueberry")
712
+
713
+ def test_list_all_supports_has(self):
714
+ self.create_object({"flavor": "strawberry"})
715
+ self.create_object({"flavor": "blueberry", "author": None})
716
+ self.create_object({"flavor": "raspberry", "author": ""})
717
+ self.create_object({"flavor": "watermelon", "author": "hello"})
718
+ self.create_object({"flavor": "pineapple", "author": "null"})
719
+ filters = [Filter("author", True, utils.COMPARISON.HAS)]
720
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
721
+ self.assertEqual(len(objects), 4)
722
+ self.assertEqual(
723
+ sorted([r["flavor"] for r in objects]),
724
+ ["blueberry", "pineapple", "raspberry", "watermelon"],
725
+ )
726
+
727
+ filters = [Filter("author", False, utils.COMPARISON.HAS)]
728
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
729
+ self.assertEqual(len(objects), 1)
730
+ self.assertEqual(objects[0]["flavor"], "strawberry")
731
+
732
+ def test_list_all_can_filter_by_subobjects_values(self):
733
+ for code in ["a", "b", "c"]:
734
+ self.create_object({"code": {"sub": code}})
735
+ filters = [Filter("code.sub", "a", utils.COMPARISON.EQ)]
736
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
737
+ self.assertEqual(len(objects), 1)
738
+
739
+ def test_list_all_can_filter_with_like_and_implicit_wildchars(self):
740
+ self.create_object({"name": "foo"})
741
+ self.create_object({"name": "aafooll"})
742
+ self.create_object({"name": "bar"})
743
+ self.create_object({"name": "FOOBAR"})
744
+
745
+ filters = [Filter("name", "FoO", utils.COMPARISON.LIKE)]
746
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
747
+ self.assertEqual(len(objects), 3)
748
+
749
+ def test_list_all_can_filter_with_wildchars(self):
750
+ self.create_object({"name": "eabcg"})
751
+ self.create_object({"name": "aabcc"})
752
+ self.create_object({"name": "abc"})
753
+ self.create_object({"name": "aec"})
754
+ self.create_object({"name": "efg"})
755
+
756
+ filters = [Filter("name", "a*b*c", utils.COMPARISON.LIKE)]
757
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
758
+ self.assertEqual(len(objects), 2)
759
+
760
+ def test_objects_filtered_when_searched_by_string_field(self):
761
+ self.create_object({"name": "foo"})
762
+ self.create_object({"name": "bar"})
763
+ self.create_object({"name": "FOOBAR"})
764
+
765
+ filters = [Filter("name", "FoO", utils.COMPARISON.LIKE)]
766
+ results = self.storage.list_all(filters=filters, **self.storage_kw)
767
+ self.assertEqual(len(results), 2)
768
+
769
+
770
+ class TimestampsTest:
771
+ def test_timestamp_are_incremented_on_create(self):
772
+ self.create_object() # init
773
+ before = self.storage.resource_timestamp(**self.storage_kw)
774
+ self.create_object()
775
+ after = self.storage.resource_timestamp(**self.storage_kw)
776
+ self.assertTrue(before < after)
777
+
778
+ def test_timestamp_are_incremented_on_update(self):
779
+ stored = self.create_object()
780
+ _id = stored["id"]
781
+ before = self.storage.resource_timestamp(**self.storage_kw)
782
+ self.storage.update(object_id=_id, obj={"bar": "foo"}, **self.storage_kw)
783
+ after = self.storage.resource_timestamp(**self.storage_kw)
784
+ self.assertTrue(before < after)
785
+
786
+ def test_timestamp_are_incremented_on_delete(self):
787
+ stored = self.create_object()
788
+ _id = stored["id"]
789
+ before = self.storage.resource_timestamp(**self.storage_kw)
790
+ self.storage.delete(object_id=_id, **self.storage_kw)
791
+ after = self.storage.resource_timestamp(**self.storage_kw)
792
+ self.assertTrue(before < after)
793
+
794
+ def test_all_timestamps_by_parent_id(self):
795
+ self.storage.create(obj={"id": "main"}, resource_name="bucket", parent_id="")
796
+ self.storage.create(obj={"id": "cid1"}, resource_name="collection", parent_id="/main")
797
+ self.storage.create(obj={"id": "cid2"}, resource_name="collection", parent_id="/main")
798
+ self.storage.create(obj={}, resource_name="record", parent_id="/main/cid2")
799
+ self.storage.create(obj={}, resource_name="record", parent_id="/main/cid2")
800
+
801
+ self.assertEqual(
802
+ {
803
+ "": self.storage.resource_timestamp(resource_name="bucket", parent_id=""),
804
+ },
805
+ self.storage.all_resources_timestamps(resource_name="bucket"),
806
+ )
807
+ self.assertEqual(
808
+ {
809
+ "/main": self.storage.resource_timestamp(
810
+ resource_name="collection", parent_id="/main"
811
+ ),
812
+ },
813
+ self.storage.all_resources_timestamps(resource_name="collection"),
814
+ )
815
+ self.assertEqual(
816
+ {
817
+ "/main/cid1": self.storage.resource_timestamp(
818
+ resource_name="record", parent_id="/main/cid1"
819
+ ),
820
+ "/main/cid2": self.storage.resource_timestamp(
821
+ resource_name="record", parent_id="/main/cid2"
822
+ ),
823
+ },
824
+ self.storage.all_resources_timestamps(resource_name="record"),
825
+ )
826
+
827
+ @skip_if_ci
828
+ def test_timestamps_are_unique(self): # pragma: no cover
829
+ obtained = []
830
+
831
+ def create_item():
832
+ for i in range(100):
833
+ obj = self.create_object()
834
+ obtained.append((obj["last_modified"], obj["id"]))
835
+
836
+ thread1 = self._create_thread(target=create_item)
837
+ thread2 = self._create_thread(target=create_item)
838
+ thread1.start()
839
+ thread2.start()
840
+ thread1.join()
841
+ thread2.join()
842
+
843
+ # With CPython (GIL), list appending is thread-safe
844
+ self.assertEqual(len(obtained), 200)
845
+ # No duplicated timestamps
846
+ self.assertEqual(len(set(obtained)), len(obtained))
847
+
848
+ def test_the_timestamp_is_not_updated_when_resource_remains_empty(self):
849
+ # Get timestamp once.
850
+ first = self.storage.resource_timestamp(**self.storage_kw)
851
+
852
+ time.sleep(0.002) # wait some time.
853
+
854
+ # Check that second time returns the same value.
855
+ second = self.storage.resource_timestamp(**self.storage_kw)
856
+ self.assertEqual(first, second)
857
+
858
+ def test_the_timestamp_are_based_on_real_time_milliseconds(self):
859
+ before = utils.msec_time()
860
+ time.sleep(0.002) # 2 msec
861
+ obj = self.create_object()
862
+ now = obj["last_modified"]
863
+ time.sleep(0.002) # 2 msec
864
+ after = utils.msec_time()
865
+ self.assertTrue(before < now < after, f"{before} < {now} < {after}")
866
+
867
+ def test_timestamp_are_always_incremented_above_existing_value(self):
868
+ # Create an object with normal clock
869
+ obj = self.create_object()
870
+ current = obj["last_modified"]
871
+
872
+ # Patch the clock to return a time in the past, before the big bang
873
+ with mock.patch("kinto.core.utils.msec_time") as time_mocked:
874
+ time_mocked.return_value = -1
875
+
876
+ obj = self.create_object()
877
+ after = obj["last_modified"]
878
+
879
+ # Expect the last one to be based on the highest value
880
+ self.assertTrue(0 < current < after, f"0 < {current} < {after}")
881
+
882
+ def test_resource_timestamp_raises_error_when_empty_and_readonly(self):
883
+ kw = {**self.storage_kw, "resource_name": "will-be-empty"}
884
+ self.storage.readonly = True
885
+ with self.assertRaises(exceptions.BackendError):
886
+ self.storage.resource_timestamp(**kw)
887
+ self.storage.readonly = False
888
+
889
+ def test_resource_timestamp_returns_current_while_readonly(self):
890
+ kw = {**self.storage_kw, "resource_name": "will-be-empty"}
891
+ ts1 = self.storage.resource_timestamp(**kw)
892
+ self.storage.readonly = True
893
+ ts2 = self.storage.resource_timestamp(**kw)
894
+ self.assertEqual(ts1, ts2)
895
+ self.storage.readonly = False
896
+
897
+ def test_create_uses_specified_last_modified_if_resource_empty(self):
898
+ # Resource is empty, create a new object that contains a timestamp.
899
+ last_modified = 1_448_881_675_541
900
+ obj = {**self.obj, self.id_field: OBJECT_ID, self.modified_field: last_modified}
901
+ self.create_object(obj=obj)
902
+
903
+ # Check that the object was assigned the specified timestamp.
904
+ retrieved = self.storage.get(object_id=OBJECT_ID, **self.storage_kw)
905
+ self.assertEqual(retrieved[self.modified_field], last_modified)
906
+
907
+ # Resource timestamp is now the same as its only object.
908
+ resource_ts = self.storage.resource_timestamp(**self.storage_kw)
909
+ self.assertEqual(resource_ts, last_modified)
910
+
911
+ def test_create_ignores_specified_last_modified_if_in_the_past(self):
912
+ # Create a first object, and get the timestamp.
913
+ first_object = self.create_object()
914
+ timestamp_before = first_object[self.modified_field]
915
+
916
+ # Create a new object with its timestamp in the past.
917
+ obj = {**self.obj, self.id_field: OBJECT_ID, self.modified_field: timestamp_before - 10}
918
+ self.create_object(obj=obj)
919
+
920
+ # Check that object timestamp is the one specified.
921
+ retrieved = self.storage.get(object_id=OBJECT_ID, **self.storage_kw)
922
+ self.assertLess(retrieved[self.modified_field], timestamp_before)
923
+ self.assertEqual(retrieved[self.modified_field], obj[self.modified_field])
924
+
925
+ # Check that resource timestamp was not changed. Someone importing
926
+ # objects in the past must assume the synchronization consequences.
927
+ timestamp = self.storage.resource_timestamp(**self.storage_kw)
928
+ self.assertEqual(timestamp, timestamp_before)
929
+
930
+ def test_create_ignores_specified_last_modified_if_equal(self):
931
+ # Create a first object, and get the timestamp.
932
+ first_object = self.create_object()
933
+ timestamp_before = first_object[self.modified_field]
934
+
935
+ # Create a new object with its timestamp in the past.
936
+ obj = {**self.obj, self.id_field: OBJECT_ID, self.modified_field: timestamp_before}
937
+ self.create_object(obj=obj)
938
+
939
+ # Check that object timestamp is the one specified.
940
+ retrieved = self.storage.get(object_id=OBJECT_ID, **self.storage_kw)
941
+ self.assertGreater(retrieved[self.modified_field], timestamp_before)
942
+ self.assertGreater(retrieved[self.modified_field], obj[self.modified_field])
943
+
944
+ # Check that resource timestamp was bumped (change happened).
945
+ timestamp = self.storage.resource_timestamp(**self.storage_kw)
946
+ self.assertGreater(timestamp, timestamp_before)
947
+
948
+ def test_update_uses_specified_last_modified_if_in_future(self):
949
+ stored = self.create_object()
950
+ object_id = stored[self.id_field]
951
+ timestamp_before = stored[self.modified_field]
952
+
953
+ # Set timestamp manually in the future.
954
+ stored[self.modified_field] = timestamp_before + 10
955
+ self.storage.update(object_id=object_id, obj=stored, **self.storage_kw)
956
+
957
+ # Check that object timestamp is the one specified.
958
+ retrieved = self.storage.get(object_id=object_id, **self.storage_kw)
959
+ self.assertGreater(retrieved[self.modified_field], timestamp_before)
960
+ self.assertGreaterEqual(retrieved[self.modified_field], stored[self.modified_field])
961
+
962
+ # Check that resource timestamp took the one specified (in future).
963
+ timestamp = self.storage.resource_timestamp(**self.storage_kw)
964
+ self.assertGreater(timestamp, timestamp_before)
965
+ self.assertEqual(timestamp, retrieved[self.modified_field])
966
+
967
+ def test_update_ignores_specified_last_modified_if_in_the_past(self):
968
+ stored = self.create_object()
969
+ object_id = stored[self.id_field]
970
+ timestamp_before = self.storage.resource_timestamp(**self.storage_kw)
971
+
972
+ # Set timestamp manually in the past.
973
+ stored[self.modified_field] = timestamp_before - 10
974
+ self.storage.update(object_id=object_id, obj=stored, **self.storage_kw)
975
+
976
+ # Check that object timestamp is the one specified.
977
+ retrieved = self.storage.get(object_id=object_id, **self.storage_kw)
978
+ self.assertLess(retrieved[self.modified_field], timestamp_before)
979
+ self.assertEqual(retrieved[self.modified_field], stored[self.modified_field])
980
+
981
+ # Check that resource timestamp was not changed. Someone importing
982
+ # objects in the past must assume the synchronization consequences.
983
+ timestamp = self.storage.resource_timestamp(**self.storage_kw)
984
+ self.assertEqual(timestamp, timestamp_before)
985
+
986
+ def test_update_ignores_specified_last_modified_if_equal(self):
987
+ stored = self.create_object()
988
+ object_id = stored[self.id_field]
989
+ timestamp_before = stored[self.modified_field]
990
+
991
+ # Do not change the timestamp.
992
+ self.storage.update(object_id=object_id, obj=stored, **self.storage_kw)
993
+
994
+ # Check that object timestamp was bumped.
995
+ retrieved = self.storage.get(object_id=object_id, **self.storage_kw)
996
+ self.assertGreater(retrieved[self.modified_field], timestamp_before)
997
+ self.assertGreater(retrieved[self.modified_field], stored[self.modified_field])
998
+
999
+ # Check that resource timestamp was bumped (change happened).
1000
+ timestamp = self.storage.resource_timestamp(**self.storage_kw)
1001
+ self.assertGreater(timestamp, timestamp_before)
1002
+
1003
+ def test_legacy_get_all_works_with_deprecation_warning(self):
1004
+ self.create_object(parent_id="abc", resource_name="c")
1005
+ obj = self.create_object(parent_id="abc", resource_name="c")
1006
+ self.storage.delete(object_id=obj["id"], parent_id="abc", resource_name="c")
1007
+ self.create_object(parent_id="abe", resource_name="c")
1008
+
1009
+ pagination = [[Filter("last_modified", 314, utils.COMPARISON.GT)]]
1010
+ sorting = [Sort("last_modified", 1)]
1011
+ objects, count = self.storage.get_all(
1012
+ parent_id="ab*",
1013
+ resource_name="c",
1014
+ include_deleted=True,
1015
+ # sorting, limits, and pagination doesn't make sense for counting but it should
1016
+ # not complain when you use the legacy get_all.
1017
+ sorting=sorting,
1018
+ pagination_rules=pagination,
1019
+ limit=99,
1020
+ )
1021
+ self.assertEqual(len(objects), 3)
1022
+ self.assertEqual(count, 2)
1023
+ print(self.mocked_warnings.mock_calls)
1024
+ message = "Use either self.list_all() or self.count_all()"
1025
+ self.mocked_warnings.assert_called_with(message, DeprecationWarning)
1026
+
1027
+
1028
+ class DeletedObjectsTest:
1029
+ def _get_last_modified_filters(self):
1030
+ start = self.storage.resource_timestamp(**self.storage_kw)
1031
+ time.sleep(0.1)
1032
+ return [Filter(self.modified_field, start, utils.COMPARISON.GT)]
1033
+
1034
+ def create_and_delete_object(self, obj=None):
1035
+ """Helper to create and delete an object."""
1036
+ obj = obj or {"challenge": "accepted"}
1037
+ obj = self.create_object(obj)
1038
+ time.sleep(0.001) # 1 msec
1039
+ deleted = self.storage.delete(object_id=obj["id"], **self.storage_kw)
1040
+ time.sleep(0.001) # 1 msec
1041
+ return deleted
1042
+
1043
+ def test_get_should_not_return_deleted_items(self):
1044
+ obj = self.create_and_delete_object()
1045
+ self.assertRaises(
1046
+ exceptions.ObjectNotFoundError,
1047
+ self.storage.get,
1048
+ object_id=obj["id"],
1049
+ **self.storage_kw,
1050
+ )
1051
+
1052
+ def test_deleting_a_deleted_item_should_raise_not_found(self):
1053
+ obj = self.create_and_delete_object()
1054
+ self.assertRaises(
1055
+ exceptions.ObjectNotFoundError,
1056
+ self.storage.delete,
1057
+ object_id=obj["id"],
1058
+ **self.storage_kw,
1059
+ )
1060
+
1061
+ def test_recreating_a_deleted_object_should_delete_its_tombstone(self):
1062
+ obj = {"id": "jesus", "rebirth": True}
1063
+ self.create_and_delete_object(obj)
1064
+ self.create_object(obj)
1065
+ objects = self.storage.list_all(include_deleted=True, **self.storage_kw)
1066
+ self.assertEqual(len(objects), 1) # No tombstone.
1067
+ count = self.storage.count_all(**self.storage_kw)
1068
+ self.assertEqual(count, 1) # One existing.
1069
+
1070
+ def test_deleting_a_object_twice_should_update_its_tombstone(self):
1071
+ obj = {"id": "jesus", "rebirth": True}
1072
+ deleted = self.create_and_delete_object(obj)
1073
+ before = deleted["last_modified"]
1074
+ deleted = self.create_and_delete_object(obj)
1075
+ after = deleted["last_modified"]
1076
+ self.assertNotEqual(before, after)
1077
+
1078
+ def test_deleted_items_have_deleted_set_to_true(self):
1079
+ obj = self.create_and_delete_object()
1080
+ self.assertTrue(obj["deleted"])
1081
+
1082
+ def test_deleted_items_have_only_basic_fields(self):
1083
+ obj = self.create_and_delete_object()
1084
+ self.assertIn("id", obj)
1085
+ self.assertIn("last_modified", obj)
1086
+ self.assertNotIn("challenge", obj)
1087
+
1088
+ def test_last_modified_of_a_deleted_item_is_deletion_time(self):
1089
+ before = self.storage.resource_timestamp(**self.storage_kw)
1090
+ obj = self.create_and_delete_object()
1091
+ now = self.storage.resource_timestamp(**self.storage_kw)
1092
+ self.assertEqual(now, obj["last_modified"])
1093
+ self.assertTrue(before < obj["last_modified"])
1094
+
1095
+ def test_list_all_does_not_include_deleted_items_by_default(self):
1096
+ self.create_and_delete_object()
1097
+ objects = self.storage.list_all(**self.storage_kw)
1098
+ self.assertEqual(len(objects), 0)
1099
+
1100
+ def test_list_all_count_does_not_include_deleted_items(self):
1101
+ filters = self._get_last_modified_filters()
1102
+ self.create_and_delete_object()
1103
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1104
+ self.assertEqual(len(objects), 1)
1105
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1106
+ self.assertEqual(count, 0)
1107
+
1108
+ def test_list_all_can_return_deleted_items(self):
1109
+ filters = self._get_last_modified_filters()
1110
+ obj = self.create_and_delete_object()
1111
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1112
+ deleted = objects[0]
1113
+ self.assertEqual(deleted["id"], obj["id"])
1114
+ self.assertEqual(deleted["last_modified"], obj["last_modified"])
1115
+ self.assertEqual(deleted["deleted"], True)
1116
+ self.assertNotIn("challenge", deleted)
1117
+
1118
+ def test_delete_all_keeps_track_of_deleted_objects(self):
1119
+ filters = self._get_last_modified_filters()
1120
+ obj = {"challenge": "accepted"}
1121
+ obj = self.create_object(obj)
1122
+ self.storage.delete_all(**self.storage_kw)
1123
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1124
+ self.assertEqual(len(objects), 1)
1125
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1126
+ self.assertEqual(count, 0)
1127
+
1128
+ def test_delete_all_can_delete_without_tombstones(self):
1129
+ # Create 2 objects, one becomes tombstone.
1130
+ filters = self._get_last_modified_filters()
1131
+ r = self.create_and_delete_object()
1132
+ self.create_object({"challenge": "accepted"})
1133
+
1134
+ # Delete objects, without creating new tombstones.
1135
+ old = self.storage.delete_all(filters=filters, with_deleted=False, **self.storage_kw)
1136
+ self.assertEqual(len(old), 1) # Not 2, because one is tombstone.
1137
+
1138
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1139
+ self.assertEqual(len(objects), 1)
1140
+ self.assertTrue(objects[0]["deleted"])
1141
+ self.assertTrue(objects[0]["id"], r["id"])
1142
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1143
+ self.assertEqual(count, 0)
1144
+
1145
+ def test_delete_can_delete_without_tombstones(self):
1146
+ filters = self._get_last_modified_filters()
1147
+ obj = {"challenge": "accepted"}
1148
+ obj = self.create_object(obj)
1149
+ self.storage.delete(object_id=obj["id"], with_deleted=False, **self.storage_kw)
1150
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1151
+ self.assertEqual(len(objects), 0)
1152
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1153
+ self.assertEqual(count, 0)
1154
+
1155
+ def test_deleting_without_tombstone_should_raise_not_found(self):
1156
+ obj = self.create_and_delete_object()
1157
+ self.assertRaises(
1158
+ exceptions.ObjectNotFoundError,
1159
+ self.storage.delete,
1160
+ object_id=obj["id"],
1161
+ with_deleted=False,
1162
+ **self.storage_kw,
1163
+ )
1164
+
1165
+ def test_delete_all_deletes_objects(self):
1166
+ self.create_object()
1167
+ self.create_object()
1168
+
1169
+ self.storage.delete_all(**self.storage_kw)
1170
+
1171
+ count = self.storage.count_all(**self.storage_kw)
1172
+ self.assertEqual(count, 0)
1173
+
1174
+ def test_delete_all_bumps_collection_timestamp(self):
1175
+ self.create_object()
1176
+ self.create_object()
1177
+ timestamp_before = self.storage.resource_timestamp(**self.storage_kw)
1178
+
1179
+ self.storage.delete_all(**self.storage_kw)
1180
+
1181
+ timestamp_after = self.storage.resource_timestamp(**self.storage_kw)
1182
+ self.assertNotEqual(timestamp_after, timestamp_before)
1183
+
1184
+ def test_delete_all_keeps_tombstones(self):
1185
+ self.create_object()
1186
+ self.create_object()
1187
+
1188
+ self.storage.delete_all(**self.storage_kw)
1189
+
1190
+ self.assertEqual(len(self.storage.list_all(include_deleted=True, **self.storage_kw)), 2)
1191
+
1192
+ def test_delete_all_bumps_tombstones_timestamps(self):
1193
+ self.create_object()
1194
+ self.create_object()
1195
+ timestamps_before = {r["last_modified"] for r in self.storage.list_all(**self.storage_kw)}
1196
+
1197
+ self.storage.delete_all(**self.storage_kw)
1198
+
1199
+ timestamps_after = {
1200
+ r["last_modified"]
1201
+ for r in self.storage.list_all(include_deleted=True, **self.storage_kw)
1202
+ }
1203
+ self.assertTrue(timestamps_after.isdisjoint(timestamps_before))
1204
+
1205
+ def test_delete_all_can_delete_by_parent_id(self):
1206
+ self.create_object(parent_id="abc", resource_name="c")
1207
+ self.create_object(parent_id="abc", resource_name="c")
1208
+ self.create_object(parent_id="efg", resource_name="c")
1209
+ self.storage.delete_all(parent_id="ab*", resource_name=None, with_deleted=False)
1210
+ objects = self.storage.list_all(parent_id="abc", resource_name="c", include_deleted=True)
1211
+ self.assertEqual(len(objects), 0)
1212
+ count = self.storage.count_all(parent_id="abc", resource_name="c")
1213
+ self.assertEqual(count, 0)
1214
+ objects = self.storage.list_all(parent_id="efg", resource_name="c", include_deleted=True)
1215
+ self.assertEqual(len(objects), 1)
1216
+ count = self.storage.count_all(parent_id="efg", resource_name="c")
1217
+ self.assertEqual(count, 1)
1218
+
1219
+ def test_delete_all_does_proper_parent_id_matching(self):
1220
+ self.create_object(parent_id="abc", resource_name="c")
1221
+ self.create_object(parent_id="xabcx", resource_name="c")
1222
+ self.create_object(parent_id="efg", resource_name="c")
1223
+ self.storage.delete_all(parent_id="ab*", resource_name=None, with_deleted=False)
1224
+ objects = self.storage.list_all(parent_id="xabcx", resource_name="c", include_deleted=True)
1225
+ self.assertEqual(len(objects), 1)
1226
+ count = self.storage.count_all(parent_id="xabcx", resource_name="c")
1227
+ self.assertEqual(count, 1)
1228
+ objects = self.storage.list_all(parent_id="efg", resource_name="c", include_deleted=True)
1229
+ self.assertEqual(len(objects), 1)
1230
+ count = self.storage.count_all(parent_id="efg", resource_name="c")
1231
+ self.assertEqual(count, 1)
1232
+
1233
+ def test_delete_all_does_proper_matching(self):
1234
+ self.create_object(parent_id="abc", resource_name="c", obj={"id": "id1"})
1235
+ self.create_object(parent_id="def", resource_name="g", obj={"id": "id1"})
1236
+ self.storage.delete_all(parent_id="ab*", resource_name=None, with_deleted=False)
1237
+ objects = self.storage.list_all(parent_id="def", resource_name="g", include_deleted=True)
1238
+ self.assertEqual(len(objects), 1)
1239
+ count = self.storage.count_all(parent_id="def", resource_name="g")
1240
+ self.assertEqual(count, 1)
1241
+
1242
+ def test_delete_all_can_delete_by_parent_id_with_tombstones(self):
1243
+ self.create_object(parent_id="abc", resource_name="c")
1244
+ self.create_object(parent_id="abc", resource_name="c")
1245
+ self.create_object(parent_id="efg", resource_name="c")
1246
+ self.storage.delete_all(parent_id="ab*", resource_name=None, with_deleted=True)
1247
+ objects = self.storage.list_all(parent_id="efg", resource_name="c", include_deleted=True)
1248
+ self.assertEqual(len(objects), 1)
1249
+ count = self.storage.count_all(parent_id="efg", resource_name="c")
1250
+ self.assertEqual(count, 1)
1251
+
1252
+ objects = self.storage.list_all(parent_id="abc", resource_name="c", include_deleted=True)
1253
+ self.assertEqual(len(objects), 2)
1254
+ self.assertTrue(objects[0]["deleted"])
1255
+ self.assertTrue(objects[1]["deleted"])
1256
+ count = self.storage.count_all(parent_id="abc", resource_name="c")
1257
+ self.assertEqual(count, 0)
1258
+
1259
+ def test_delete_all_can_delete_partially(self):
1260
+ self.create_object({"foo": "po"})
1261
+ self.create_object()
1262
+ filters = [Filter("foo", "bar", utils.COMPARISON.EQ)]
1263
+ self.storage.delete_all(filters=filters, **self.storage_kw)
1264
+ count = self.storage.count_all(**self.storage_kw)
1265
+ self.assertEqual(count, 1)
1266
+
1267
+ def test_delete_all_supports_limit(self):
1268
+ self.create_object()
1269
+ self.create_object()
1270
+ self.storage.delete_all(limit=1, **self.storage_kw)
1271
+ count = self.storage.count_all(**self.storage_kw)
1272
+ self.assertEqual(count, 1)
1273
+
1274
+ def test_delete_all_supports_sorting(self):
1275
+ for i in range(5):
1276
+ self.create_object({"foo": i})
1277
+ sorting = [Sort("foo", -1)]
1278
+ self.storage.delete_all(limit=2, sorting=sorting, **self.storage_kw)
1279
+ objects = self.storage.list_all(sorting=sorting, **self.storage_kw)
1280
+ self.assertEqual(objects[0]["foo"], 2)
1281
+
1282
+ def test_purge_deleted_remove_all_tombstones(self):
1283
+ self.create_object()
1284
+ self.create_object()
1285
+ self.storage.delete_all(**self.storage_kw)
1286
+ num_removed = self.storage.purge_deleted(**self.storage_kw)
1287
+ self.assertEqual(num_removed, 2)
1288
+ objects = self.storage.list_all(include_deleted=True, **self.storage_kw)
1289
+ self.assertEqual(len(objects), 0)
1290
+ count = self.storage.count_all(**self.storage_kw)
1291
+ self.assertEqual(count, 0)
1292
+
1293
+ def test_purge_deleted_remove_all_tombstones_by_parent_id(self):
1294
+ self.create_object(parent_id="abc", resource_name="c")
1295
+ self.create_object(parent_id="abc", resource_name="c")
1296
+ self.create_object(parent_id="efg", resource_name="c")
1297
+ self.storage.delete_all(parent_id="abc", resource_name="c")
1298
+ self.storage.delete_all(parent_id="efg", resource_name="c")
1299
+ num_removed = self.storage.purge_deleted(parent_id="ab*", resource_name=None)
1300
+ self.assertEqual(num_removed, 2)
1301
+
1302
+ def test_purge_deleted_removes_timestamps_by_parent_id(self):
1303
+ self.create_object(parent_id="/abc/a", resource_name="c")
1304
+ self.create_object(parent_id="/abc/a", resource_name="c")
1305
+ self.create_object(parent_id="/efg", resource_name="c")
1306
+
1307
+ all_timestamps = self.storage.all_resources_timestamps(resource_name="c")
1308
+ self.assertEqual(set(all_timestamps.keys()), {"/abc/a", "/efg"})
1309
+
1310
+ before1 = self.storage.resource_timestamp(parent_id="/abc/a", resource_name="c")
1311
+ # Different parent_id with object.
1312
+ before2 = self.storage.resource_timestamp(parent_id="/efg", resource_name="c")
1313
+ # Different parent_id without object.
1314
+ before3 = self.storage.resource_timestamp(parent_id="/ijk", resource_name="c")
1315
+
1316
+ self.storage.delete_all(parent_id="/abc/*", resource_name=None, with_deleted=False)
1317
+ self.storage.purge_deleted(parent_id="/abc/*", resource_name=None)
1318
+
1319
+ all_timestamps = self.storage.all_resources_timestamps(resource_name="c")
1320
+ self.assertEqual(set(all_timestamps.keys()), {"/efg", "/ijk"})
1321
+
1322
+ time.sleep(0.002) # make sure we don't recreate timestamps at same msec.
1323
+ after1 = self.storage.resource_timestamp(parent_id="/abc/a", resource_name="c")
1324
+ after2 = self.storage.resource_timestamp(parent_id="/efg", resource_name="c")
1325
+ after3 = self.storage.resource_timestamp(parent_id="/ijk", resource_name="c")
1326
+
1327
+ self.assertNotEqual(before1, after1) # timestamp was removed, it will differ.
1328
+ self.assertEqual(before2, after2)
1329
+ self.assertEqual(before3, after3)
1330
+
1331
+ def test_purge_deleted_works_when_no_tombstones(self):
1332
+ num_removed = self.storage.purge_deleted(**self.storage_kw)
1333
+ self.assertEqual(num_removed, 0)
1334
+
1335
+ def test_purge_deleted_remove_with_before_remove_olders_exclusive(self):
1336
+ older = self.create_object()
1337
+ newer = self.create_object()
1338
+ self.storage.delete(object_id=older["id"], **self.storage_kw)
1339
+ self.storage.delete(object_id=newer["id"], **self.storage_kw)
1340
+ objects = self.storage.list_all(include_deleted=True, **self.storage_kw)
1341
+ self.assertEqual(len(objects), 2)
1342
+ count = self.storage.count_all(**self.storage_kw)
1343
+ self.assertEqual(count, 0)
1344
+ num_removed = self.storage.purge_deleted(
1345
+ before=max([r["last_modified"] for r in objects]), **self.storage_kw
1346
+ )
1347
+ self.assertEqual(num_removed, 1)
1348
+ objects = self.storage.list_all(include_deleted=True, **self.storage_kw)
1349
+ self.assertEqual(len(objects), 1)
1350
+ count = self.storage.count_all(**self.storage_kw)
1351
+ self.assertEqual(count, 0)
1352
+
1353
+ def test_purge_deleted_does_not_support_before_and_max_retained(self):
1354
+ self.assertRaises(
1355
+ ValueError,
1356
+ self.storage.purge_deleted,
1357
+ resource_name="r",
1358
+ parent_id="p",
1359
+ before=42,
1360
+ max_retained=1,
1361
+ )
1362
+
1363
+ def test_purge_deleted_remove_with_max_count_per_collection(self):
1364
+ cid1_kw = dict(parent_id="cid1", resource_name="one")
1365
+ for i in range(5):
1366
+ record = self.create_object(**cid1_kw)
1367
+ self.storage.delete(object_id=record["id"], **cid1_kw)
1368
+ cid2_kw = dict(parent_id="cid2", resource_name="one")
1369
+ for i in range(4):
1370
+ record = self.create_object(**cid2_kw)
1371
+ self.storage.delete(object_id=record["id"], **cid2_kw)
1372
+ cid1_other_kw = dict(parent_id="cid1", resource_name="other")
1373
+ for i in range(5):
1374
+ record = self.create_object(**cid1_other_kw)
1375
+ self.storage.delete(object_id=record["id"], **cid1_other_kw)
1376
+
1377
+ # Consistency checks first.
1378
+ objects_c1_before = self.storage.list_all(include_deleted=True, **cid1_kw)
1379
+ self.assertEqual(len(objects_c1_before), 5)
1380
+ objects_c2_before = self.storage.list_all(include_deleted=True, **cid2_kw)
1381
+ self.assertEqual(len(objects_c2_before), 4)
1382
+ objects_c1_other_before = self.storage.list_all(include_deleted=True, **cid1_other_kw)
1383
+ self.assertEqual(len(objects_c1_other_before), 5)
1384
+
1385
+ num_removed = self.storage.purge_deleted(
1386
+ resource_name="one", parent_id="*", max_retained=3
1387
+ )
1388
+
1389
+ self.assertEqual(num_removed, 3) # 2 for one/cid1, 1 for one/cid2, 0 for other/cid1
1390
+
1391
+ # It kept 3 tombstones for each resource/parent.
1392
+ objects = self.storage.list_all(include_deleted=True, **cid1_kw)
1393
+ self.assertEqual(len(objects), 3)
1394
+ self.assertEqual(
1395
+ min(obj["last_modified"] for obj in objects), objects_c1_before[2]["last_modified"]
1396
+ )
1397
+
1398
+ objects = self.storage.list_all(include_deleted=True, **cid2_kw)
1399
+ self.assertEqual(len(objects), 3)
1400
+ self.assertNotEqual(
1401
+ min(obj["last_modified"] for obj in objects), objects_c2_before[2]["last_modified"]
1402
+ )
1403
+
1404
+ objects = self.storage.list_all(include_deleted=True, **cid1_other_kw)
1405
+ self.assertEqual(len(objects), 5)
1406
+
1407
+ #
1408
+ # Sorting
1409
+ #
1410
+
1411
+ def test_sorting_on_last_modified_applies_to_deleted_items(self):
1412
+ filters = self._get_last_modified_filters()
1413
+ first = last = None
1414
+ for i in range(20, 0, -1):
1415
+ obj = self.create_and_delete_object()
1416
+ first = obj if i == 1 else first
1417
+ last = obj if i == 20 else last
1418
+
1419
+ sorting = [Sort("last_modified", -1)]
1420
+ objects = self.storage.list_all(
1421
+ sorting=sorting, filters=filters, include_deleted=True, **self.storage_kw
1422
+ )
1423
+
1424
+ self.assertDictEqual(objects[0], first)
1425
+ self.assertDictEqual(objects[-1], last)
1426
+
1427
+ def test_sorting_on_last_modified_mixes_deleted_objects(self):
1428
+ filters = self._get_last_modified_filters()
1429
+ self.create_and_delete_object()
1430
+ self.create_object()
1431
+ self.create_and_delete_object()
1432
+
1433
+ sorting = [Sort("last_modified", 1)]
1434
+ objects = self.storage.list_all(
1435
+ sorting=sorting, filters=filters, include_deleted=True, **self.storage_kw
1436
+ )
1437
+
1438
+ self.assertIn("deleted", objects[0])
1439
+ self.assertNotIn("deleted", objects[1])
1440
+ self.assertIn("deleted", objects[2])
1441
+
1442
+ def test_sorting_on_arbitrary_field_groups_deleted_at_last(self):
1443
+ filters = self._get_last_modified_filters()
1444
+ self.create_object({"status": 0})
1445
+ self.create_and_delete_object({"status": 1})
1446
+ self.create_and_delete_object({"status": 2})
1447
+
1448
+ sorting = [Sort("status", 1)]
1449
+ objects = self.storage.list_all(
1450
+ sorting=sorting, filters=filters, include_deleted=True, **self.storage_kw
1451
+ )
1452
+ self.assertNotIn("deleted", objects[0])
1453
+ self.assertIn("deleted", objects[1])
1454
+ self.assertIn("deleted", objects[2])
1455
+
1456
+ def test_support_sorting_on_deleted_field_groups_deleted_at_first(self):
1457
+ filters = self._get_last_modified_filters()
1458
+ # Respect boolean sort order
1459
+ self.create_and_delete_object()
1460
+ self.create_object()
1461
+ self.create_and_delete_object()
1462
+
1463
+ sorting = [Sort("deleted", 1)]
1464
+ objects = self.storage.list_all(
1465
+ sorting=sorting, filters=filters, include_deleted=True, **self.storage_kw
1466
+ )
1467
+ self.assertIn("deleted", objects[0])
1468
+ self.assertIn("deleted", objects[1])
1469
+ self.assertNotIn("deleted", objects[2])
1470
+
1471
+ def test_sorting_on_numeric_arbitrary_field(self):
1472
+ filters = self._get_last_modified_filters()
1473
+ for code in [1, 10, 6, 46]:
1474
+ self.create_object({"status": code})
1475
+
1476
+ sorting = [Sort("status", -1)]
1477
+ objects = self.storage.list_all(
1478
+ sorting=sorting, filters=filters, include_deleted=True, **self.storage_kw
1479
+ )
1480
+ self.assertEqual(objects[0]["status"], 46)
1481
+ self.assertEqual(objects[1]["status"], 10)
1482
+ self.assertEqual(objects[2]["status"], 6)
1483
+ self.assertEqual(objects[3]["status"], 1)
1484
+
1485
+ #
1486
+ # Filtering
1487
+ #
1488
+
1489
+ def test_filtering_on_last_modified_applies_to_deleted_items(self):
1490
+ self.create_and_delete_object()
1491
+ filters = self._get_last_modified_filters()
1492
+ self.create_object()
1493
+ self.create_and_delete_object()
1494
+
1495
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1496
+ self.assertEqual(len(objects), 2)
1497
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1498
+ self.assertEqual(count, 1)
1499
+
1500
+ def test_filtering_on_arbitrary_field_excludes_deleted_objects(self):
1501
+ filters = self._get_last_modified_filters()
1502
+ self.create_object({"status": 0})
1503
+ self.create_and_delete_object({"status": 0})
1504
+
1505
+ filters += [Filter("status", 0, utils.COMPARISON.EQ)]
1506
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
1507
+ self.assertEqual(len(objects), 1)
1508
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1509
+ self.assertEqual(count, 1)
1510
+
1511
+ def test_support_filtering_on_deleted_field(self):
1512
+ filters = self._get_last_modified_filters()
1513
+ self.create_object()
1514
+ self.create_and_delete_object()
1515
+
1516
+ filters += [Filter("deleted", True, utils.COMPARISON.EQ)]
1517
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1518
+ self.assertIn("deleted", objects[0])
1519
+ self.assertEqual(len(objects), 1)
1520
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1521
+ self.assertEqual(count, 0)
1522
+
1523
+ def test_support_filtering_out_on_deleted_field(self):
1524
+ filters = self._get_last_modified_filters()
1525
+ self.create_object()
1526
+ self.create_and_delete_object()
1527
+
1528
+ filters += [Filter("deleted", True, utils.COMPARISON.NOT)]
1529
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1530
+ self.assertNotIn("deleted", objects[0])
1531
+ self.assertEqual(len(objects), 1)
1532
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1533
+ self.assertEqual(count, 1)
1534
+
1535
+ def test_return_empty_set_if_filtering_on_deleted_false(self):
1536
+ filters = self._get_last_modified_filters()
1537
+ self.create_object()
1538
+ self.create_and_delete_object()
1539
+
1540
+ filters += [Filter("deleted", False, utils.COMPARISON.EQ)]
1541
+ objects = self.storage.list_all(filters=filters, include_deleted=True, **self.storage_kw)
1542
+ self.assertEqual(len(objects), 0)
1543
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1544
+ self.assertEqual(count, 0)
1545
+
1546
+ def test_return_empty_set_if_filtering_on_deleted_without_include(self):
1547
+ self.create_object()
1548
+ self.create_and_delete_object()
1549
+
1550
+ filters = [Filter("deleted", True, utils.COMPARISON.EQ)]
1551
+ objects = self.storage.list_all(filters=filters, **self.storage_kw)
1552
+ self.assertEqual(len(objects), 0)
1553
+ count = self.storage.count_all(filters=filters, **self.storage_kw)
1554
+ self.assertEqual(count, 0)
1555
+
1556
+ #
1557
+ # Pagination
1558
+ #
1559
+
1560
+ def test_pagination_rules_on_last_modified_apply_to_deleted_objects(self):
1561
+ filters = self._get_last_modified_filters()
1562
+ for i in range(15):
1563
+ if i % 2 == 0:
1564
+ self.create_and_delete_object()
1565
+ else:
1566
+ self.create_object()
1567
+
1568
+ pagination = [[Filter("last_modified", 314, utils.COMPARISON.GT)]]
1569
+ sorting = [Sort("last_modified", 1)]
1570
+ objects = self.storage.list_all(
1571
+ sorting=sorting,
1572
+ pagination_rules=pagination,
1573
+ limit=5,
1574
+ filters=filters,
1575
+ include_deleted=True,
1576
+ **self.storage_kw,
1577
+ )
1578
+ self.assertEqual(len(objects), 5)
1579
+ self.assertIn("deleted", objects[0])
1580
+ self.assertNotIn("deleted", objects[1])
1581
+
1582
+ def test_pagination_can_skip_everything(self):
1583
+ for i in range(5):
1584
+ self.create_object({"i": i})
1585
+
1586
+ pagination = [[Filter("i", 7, utils.COMPARISON.GT)]]
1587
+ objects = self.storage.list_all(
1588
+ pagination_rules=pagination, limit=5, include_deleted=True, **self.storage_kw
1589
+ )
1590
+ self.assertEqual(len(objects), 0)
1591
+
1592
+ def test_list_all_handle_a_pagination_rules(self):
1593
+ for x in range(10):
1594
+ obj = dict(self.obj)
1595
+ obj["number"] = x % 3
1596
+ self.create_object(obj)
1597
+
1598
+ objects = self.storage.list_all(
1599
+ limit=5,
1600
+ pagination_rules=[[Filter("number", 1, utils.COMPARISON.GT)]],
1601
+ **self.storage_kw,
1602
+ )
1603
+ self.assertEqual(len(objects), 3)
1604
+
1605
+ def test_list_all_handle_all_pagination_rules(self):
1606
+ for x in range(10):
1607
+ obj = dict(self.obj)
1608
+ obj["number"] = x % 3
1609
+ last_object = self.create_object(obj)
1610
+
1611
+ objects = self.storage.list_all(
1612
+ limit=5,
1613
+ pagination_rules=[
1614
+ [Filter("number", 1, utils.COMPARISON.GT)],
1615
+ [Filter("id", last_object["id"], utils.COMPARISON.EQ)],
1616
+ ],
1617
+ **self.storage_kw,
1618
+ )
1619
+ self.assertEqual(len(objects), 4)
1620
+
1621
+ def test_list_all_parent_id_paginates_correctly(self):
1622
+ """Verify that pagination doesn't squash or duplicate some objects"""
1623
+
1624
+ # Create objects with different parent IDs, but the same
1625
+ # object ID.
1626
+ for parent in range(10):
1627
+ parent_id = f"abc{parent}"
1628
+ self.storage.create(
1629
+ parent_id=parent_id,
1630
+ resource_name="c",
1631
+ obj={"id": "some_id", "secret_data": parent_id},
1632
+ )
1633
+
1634
+ real_objects = self.storage.list_all(parent_id="abc*", resource_name="c")
1635
+ self.assertEqual(len(real_objects), 10)
1636
+
1637
+ def sort_by_secret_data(records):
1638
+ return sorted(records, key=lambda r: r["secret_data"])
1639
+
1640
+ GT = utils.COMPARISON.GT
1641
+ LT = utils.COMPARISON.LT
1642
+ for order in [("secret_data", 1), ("secret_data", -1)]:
1643
+ sort = [Sort(*order), Sort("last_modified", -1)]
1644
+ for limit in range(1, 10):
1645
+ with self.subTest(order=order, limit=limit):
1646
+ objects = []
1647
+ pagination = None
1648
+ while True:
1649
+ page = self.storage.list_all(
1650
+ parent_id="abc*",
1651
+ resource_name="c",
1652
+ sorting=sort,
1653
+ limit=limit,
1654
+ pagination_rules=pagination,
1655
+ )
1656
+ total_objects = self.storage.count_all(parent_id="abc*", resource_name="c")
1657
+ self.assertEqual(total_objects, len(real_objects))
1658
+ objects.extend(page)
1659
+ if len(objects) == total_objects:
1660
+ break
1661
+ # This should never happen normally, but lets
1662
+ # us fail on an assert rather than an
1663
+ # IndexError.
1664
+ if not page: # pragma: nocover
1665
+ break
1666
+ # Simulate paging though the objects as
1667
+ # though following the logic in Resource._build_pagination_rules.
1668
+ last_object = page[-1]
1669
+ order_field, order_direction = order
1670
+ pagination_direction = GT if order_direction == 1 else LT
1671
+ threshhold_field = last_object[order_field]
1672
+ threshhold_lm = last_object["last_modified"]
1673
+ pagination = [
1674
+ [
1675
+ Filter(order_field, threshhold_field, utils.COMPARISON.EQ),
1676
+ Filter("last_modified", threshhold_lm, utils.COMPARISON.LT),
1677
+ ],
1678
+ [Filter(order_field, threshhold_field, pagination_direction)],
1679
+ ]
1680
+
1681
+ self.assertEqual(
1682
+ sort_by_secret_data(real_objects), sort_by_secret_data(objects)
1683
+ )
1684
+
1685
+ def test_pagination_rules_are_confined_by_parent(self):
1686
+ intermediate_ts = None
1687
+ for i in range(4):
1688
+ r = self.create_object({"foo": i}, parent_id="/a")
1689
+ if i == 3:
1690
+ intermediate_ts = r["last_modified"]
1691
+ for i in range(4):
1692
+ self.create_object({"foo": i}, parent_id="/b")
1693
+
1694
+ sort = [Sort("foo", -1), Sort("last_modified", -1)]
1695
+ pagination_rules = [
1696
+ [Filter("foo", 1, utils.COMPARISON.GT)],
1697
+ [Filter("last_modified", intermediate_ts, utils.COMPARISON.GT)],
1698
+ ]
1699
+ page = self.storage.list_all(
1700
+ resource_name="test",
1701
+ parent_id="/a",
1702
+ sorting=sort,
1703
+ limit=10,
1704
+ pagination_rules=pagination_rules,
1705
+ )
1706
+ self.assertEqual(len(page), 2)
1707
+
1708
+ def test_delete_all_supports_pagination_rules(self):
1709
+ for i in range(6):
1710
+ self.create_object({"foo": i})
1711
+
1712
+ pagination_rules = [[Filter("foo", 3, utils.COMPARISON.GT)]]
1713
+ deleted = self.storage.delete_all(
1714
+ limit=4, pagination_rules=pagination_rules, **self.storage_kw
1715
+ )
1716
+ self.assertEqual(len(deleted), 2)
1717
+
1718
+
1719
+ class ParentObjectAccessTest:
1720
+ def test_parent_cannot_access_other_parent_object(self):
1721
+ obj = self.create_object()
1722
+ self.assertRaises(
1723
+ exceptions.ObjectNotFoundError,
1724
+ self.storage.get,
1725
+ resource_name=self.storage_kw["resource_name"],
1726
+ parent_id=self.other_parent_id,
1727
+ object_id=obj["id"],
1728
+ )
1729
+
1730
+ def test_parent_cannot_delete_other_parent_object(self):
1731
+ obj = self.create_object()
1732
+ self.assertRaises(
1733
+ exceptions.ObjectNotFoundError,
1734
+ self.storage.delete,
1735
+ resource_name=self.storage_kw["resource_name"],
1736
+ parent_id=self.other_parent_id,
1737
+ object_id=obj["id"],
1738
+ )
1739
+
1740
+ def test_parent_cannot_update_other_parent_object(self):
1741
+ obj = self.create_object()
1742
+
1743
+ new_object = {"another": "object"}
1744
+ kw = {**self.storage_kw, "parent_id": self.other_parent_id}
1745
+ self.storage.update(object_id=obj["id"], obj=new_object, **kw)
1746
+
1747
+ not_updated = self.storage.get(object_id=obj["id"], **self.storage_kw)
1748
+ self.assertNotIn("another", not_updated)
1749
+
1750
+
1751
+ class SerializationTest:
1752
+ def test_create_bytes_raises(self):
1753
+ data = {"steak": "haché".encode(encoding="utf-8")}
1754
+ self.assertIsInstance(data["steak"], bytes)
1755
+ self.assertRaises(TypeError, self.create_object, data)
1756
+
1757
+ def test_update_bytes_raises(self):
1758
+ obj = self.create_object()
1759
+
1760
+ new_object = {"steak": "haché".encode(encoding="utf-8")}
1761
+ self.assertIsInstance(new_object["steak"], bytes)
1762
+
1763
+ self.assertRaises(
1764
+ TypeError, self.storage.update, object_id=obj["id"], obj=new_object, **self.storage_kw
1765
+ )
1766
+
1767
+
1768
+ class TrimObjectsTest:
1769
+ def test_trims_n_of_same_resource(self):
1770
+ for i in range(20):
1771
+ self.create_object({"num": i, "group": "a" if i % 2 == 0 else "b"})
1772
+
1773
+ num_removed = self.storage.trim_objects(
1774
+ resource_name=self.storage_kw["resource_name"],
1775
+ parent_id=self.storage_kw["parent_id"],
1776
+ filters=[Filter("group", "a", utils.COMPARISON.EQ)],
1777
+ max_objects=3,
1778
+ )
1779
+ self.assertEqual(num_removed, 20 / 2 - 3) # it kept all 'b' and the 3 latest of 'a'
1780
+ count = self.storage.count_all(**self.storage_kw)
1781
+ self.assertEqual(count, 20 - num_removed)
1782
+
1783
+
1784
+ class DeprecatedCoreNotionsTest:
1785
+ def setUp(self):
1786
+ super().setUp()
1787
+ patch = mock.patch("warnings.warn")
1788
+ self.mocked_warnings = patch.start()
1789
+
1790
+ def test_deprecated_collection_timestamp(self):
1791
+ self.storage.collection_timestamp(collection_id="test", parent_id="")
1792
+ message = "`collection_timestamp()` is deprecated, use `resource_timestamp()` instead."
1793
+ self.mocked_warnings.assert_called_with(message, DeprecationWarning)
1794
+
1795
+ def test_create_deprecated_kwargs(self):
1796
+ self.storage.create(record={}, collection_id="test", parent_id="")
1797
+
1798
+ message = "Storage.create parameter 'record' is deprecated, use 'obj' instead"
1799
+ self.mocked_warnings.assert_called_with(message, DeprecationWarning)
1800
+
1801
+ def test_get_deprecated_kwargs(self):
1802
+ self.storage.create(obj={"id": "abc"}, resource_name="test", parent_id="")
1803
+
1804
+ self.storage.get(object_id="abc", collection_id="test", parent_id="")
1805
+
1806
+ message = (
1807
+ "Storage.get parameter 'collection_id' is deprecated, use 'resource_name' instead"
1808
+ )
1809
+ self.mocked_warnings.assert_called_with(message, DeprecationWarning)
1810
+
1811
+ def test_update_deprecated_kwargs(self):
1812
+ self.storage.update(object_id="abc", record={}, collection_id="test", parent_id="")
1813
+
1814
+ message = "Storage.update parameter 'record' is deprecated, use 'obj' instead"
1815
+ self.mocked_warnings.assert_called_with(message, DeprecationWarning)
1816
+
1817
+ def test_delete_deprecated_kwargs(self):
1818
+ self.storage.create(obj={"id": "abc"}, resource_name="test", parent_id="")
1819
+
1820
+ self.storage.delete(object_id="abc", collection_id="test", parent_id="")
1821
+
1822
+ message = (
1823
+ "Storage.delete parameter 'collection_id' is deprecated, use 'resource_name' instead"
1824
+ )
1825
+ self.mocked_warnings.assert_called_with(message, DeprecationWarning)
1826
+
1827
+ def test_delete_all_deprecated_kwargs(self):
1828
+ self.storage.delete_all(collection_id="test", parent_id="")
1829
+
1830
+ message = "Storage.delete_all parameter 'collection_id' is deprecated, use 'resource_name' instead"
1831
+ self.mocked_warnings.assert_called_with(message, DeprecationWarning)
1832
+
1833
+ def test_get_all_deprecated_kwargs(self):
1834
+ r = self.storage.create(obj={"id": "abc"}, resource_name="test", parent_id="")
1835
+
1836
+ records, count = self.storage.get_all(collection_id="test", parent_id="")
1837
+
1838
+ message = "Use either self.list_all() or self.count_all()"
1839
+ self.mocked_warnings.assert_any_call(message, DeprecationWarning)
1840
+ # Check that proper `resource_name` was used (instead of `collection_id`)
1841
+ assert records == [r]
1842
+ assert count == 1
1843
+
1844
+
1845
+ class StorageTest(
1846
+ ThreadMixin,
1847
+ TimestampsTest,
1848
+ DeletedObjectsTest,
1849
+ ParentObjectAccessTest,
1850
+ SerializationTest,
1851
+ DeprecatedCoreNotionsTest,
1852
+ BaseTestStorage,
1853
+ TrimObjectsTest,
1854
+ ):
1855
+ """Compound of all storage tests."""
1856
+
1857
+ pass