localstack-snapshot 0.2.0__tar.gz → 0.3.1__tar.gz
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.
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/PKG-INFO +13 -3
- localstack_snapshot-0.3.1/README.md +35 -0
- localstack_snapshot-0.3.1/localstack_snapshot/__init__.py +1 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/prototype.py +40 -2
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/report.py +8 -1
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/transformer.py +106 -8
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/transformer_utility.py +55 -1
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/PKG-INFO +13 -3
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/pyproject.toml +1 -1
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/tests/test_snapshots.py +139 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/tests/test_transformer.py +206 -0
- localstack_snapshot-0.2.0/README.md +0 -26
- localstack_snapshot-0.2.0/localstack_snapshot/__init__.py +0 -1
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/LICENSE +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/pytest/__init__.py +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/pytest/snapshot.py +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/__init__.py +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/util/__init__.py +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/util/encoding.py +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/SOURCES.txt +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/dependency_links.txt +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/requires.txt +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/top_level.txt +0 -0
- {localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: localstack-snapshot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Extracted snapshot testing lib for LocalStack
|
|
5
5
|
Author-email: LocalStack Contributors <info@localstack.cloud>
|
|
6
6
|
License:
|
|
@@ -222,6 +222,7 @@ Requires-Dist: black==23.10.0; extra == "dev"
|
|
|
222
222
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
223
223
|
Requires-Dist: coverage[toml]>=5.0.0; extra == "dev"
|
|
224
224
|
Requires-Dist: ruff==0.1.0; extra == "dev"
|
|
225
|
+
Dynamic: license-file
|
|
225
226
|
|
|
226
227
|
Snapshot testing for pytest
|
|
227
228
|
===============================
|
|
@@ -233,10 +234,19 @@ This project is in a very early stage and will be both restructured and renamed.
|
|
|
233
234
|
|
|
234
235
|
## Quickstart
|
|
235
236
|
|
|
236
|
-
|
|
237
|
+
To install the python and other developer requirements into a venv run:
|
|
237
238
|
|
|
238
239
|
make install
|
|
239
240
|
|
|
241
|
+
### Configuration options
|
|
242
|
+
|
|
243
|
+
There's a few env vars that can be used with this project:
|
|
244
|
+
|
|
245
|
+
* `TEST_TARGET`: Set to `AWS_CLOUD` to use an externally-deployed instance when running tests.
|
|
246
|
+
* `SNAPSHOT_LEGACY_REPORT`: By default set to `0`. Can be set to `1`. This enables the legacy reporting output style.
|
|
247
|
+
* `SNAPSHOT_UDPATE`: By default set to `0`. Can be set to `1`. This enables updating the snapshot file rather than comparing with its contents.
|
|
248
|
+
* `SNAPSHOT_RAW`: By default set to `0`. Can be set to `1`. This outputs an additional snapshot file which contains the untransformed values.
|
|
249
|
+
|
|
240
250
|
## Format code
|
|
241
251
|
|
|
242
252
|
We use black and isort as code style tools.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Snapshot testing for pytest
|
|
2
|
+
===============================
|
|
3
|
+
|
|
4
|
+
Extracted snapshot testing lib for LocalStack.
|
|
5
|
+
|
|
6
|
+
This project is in a very early stage and will be both restructured and renamed.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
10
|
+
|
|
11
|
+
To install the python and other developer requirements into a venv run:
|
|
12
|
+
|
|
13
|
+
make install
|
|
14
|
+
|
|
15
|
+
### Configuration options
|
|
16
|
+
|
|
17
|
+
There's a few env vars that can be used with this project:
|
|
18
|
+
|
|
19
|
+
* `TEST_TARGET`: Set to `AWS_CLOUD` to use an externally-deployed instance when running tests.
|
|
20
|
+
* `SNAPSHOT_LEGACY_REPORT`: By default set to `0`. Can be set to `1`. This enables the legacy reporting output style.
|
|
21
|
+
* `SNAPSHOT_UDPATE`: By default set to `0`. Can be set to `1`. This enables updating the snapshot file rather than comparing with its contents.
|
|
22
|
+
* `SNAPSHOT_RAW`: By default set to `0`. Can be set to `1`. This outputs an additional snapshot file which contains the untransformed values.
|
|
23
|
+
|
|
24
|
+
## Format code
|
|
25
|
+
|
|
26
|
+
We use black and isort as code style tools.
|
|
27
|
+
To execute them, run:
|
|
28
|
+
|
|
29
|
+
make format
|
|
30
|
+
|
|
31
|
+
## Build distribution
|
|
32
|
+
|
|
33
|
+
To build a wheel and source distribution, simply run
|
|
34
|
+
|
|
35
|
+
make dist
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.1"
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/prototype.py
RENAMED
|
@@ -25,6 +25,8 @@ from .transformer_utility import TransformerUtility
|
|
|
25
25
|
SNAPSHOT_LOGGER = logging.getLogger(__name__)
|
|
26
26
|
SNAPSHOT_LOGGER.setLevel(logging.DEBUG if os.environ.get("DEBUG_SNAPSHOT") else logging.WARNING)
|
|
27
27
|
|
|
28
|
+
_SKIP_PLACEHOLDER_VALUE = "$__to_be_skipped__$"
|
|
29
|
+
|
|
28
30
|
|
|
29
31
|
class SnapshotMatchResult:
|
|
30
32
|
def __init__(self, a: dict, b: dict, key: str = ""):
|
|
@@ -218,7 +220,7 @@ class SnapshotSession:
|
|
|
218
220
|
self.skip_verification_paths = skip_verification_paths or []
|
|
219
221
|
if skip_verification_paths:
|
|
220
222
|
SNAPSHOT_LOGGER.warning(
|
|
221
|
-
|
|
223
|
+
"Snapshot verification disabled for paths: %s", skip_verification_paths
|
|
222
224
|
)
|
|
223
225
|
|
|
224
226
|
if self.update:
|
|
@@ -272,6 +274,8 @@ class SnapshotSession:
|
|
|
272
274
|
self._transform_dict_to_parseable_values(v)
|
|
273
275
|
|
|
274
276
|
if isinstance(v, str) and v.startswith("{"):
|
|
277
|
+
# Doesn't handle JSON arrays and nested JSON strings. See JsonStringTransformer.
|
|
278
|
+
# TODO for the major release consider having JSON parsing in one place only: either here or in JsonStringTransformer
|
|
275
279
|
try:
|
|
276
280
|
json_value = json.loads(v)
|
|
277
281
|
original[k] = json_value
|
|
@@ -304,7 +308,7 @@ class SnapshotSession:
|
|
|
304
308
|
try:
|
|
305
309
|
replaced_tmp[key] = json.loads(dumped_value)
|
|
306
310
|
except JSONDecodeError:
|
|
307
|
-
SNAPSHOT_LOGGER.error(
|
|
311
|
+
SNAPSHOT_LOGGER.error("could not decode json-string:\n%s", tmp)
|
|
308
312
|
return {}
|
|
309
313
|
|
|
310
314
|
return replaced_tmp
|
|
@@ -363,6 +367,23 @@ class SnapshotSession:
|
|
|
363
367
|
|
|
364
368
|
return full_path_nodes[::-1][1:] # reverse the list and remove Root()/$
|
|
365
369
|
|
|
370
|
+
def _remove_placeholder(_tmp):
|
|
371
|
+
"""Traverse the object and remove any values in a list that would be equal to the placeholder"""
|
|
372
|
+
if isinstance(_tmp, dict):
|
|
373
|
+
for k, v in _tmp.items():
|
|
374
|
+
if isinstance(v, dict):
|
|
375
|
+
_remove_placeholder(v)
|
|
376
|
+
elif isinstance(v, list):
|
|
377
|
+
_tmp[k] = _remove_placeholder(v)
|
|
378
|
+
elif isinstance(_tmp, list):
|
|
379
|
+
return [
|
|
380
|
+
_remove_placeholder(item) for item in _tmp if item != _SKIP_PLACEHOLDER_VALUE
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
return _tmp
|
|
384
|
+
|
|
385
|
+
has_placeholder = False
|
|
386
|
+
|
|
366
387
|
for path in self.skip_verification_paths:
|
|
367
388
|
matches = parse(path).find(tmp) or []
|
|
368
389
|
for m in matches:
|
|
@@ -376,7 +397,24 @@ class SnapshotSession:
|
|
|
376
397
|
helper = helper.get(p, None)
|
|
377
398
|
if not helper:
|
|
378
399
|
continue
|
|
400
|
+
|
|
379
401
|
if (
|
|
380
402
|
isinstance(helper, dict) and full_path[-1] in helper.keys()
|
|
381
403
|
): # might have been deleted already
|
|
382
404
|
del helper[full_path[-1]]
|
|
405
|
+
elif isinstance(helper, list):
|
|
406
|
+
try:
|
|
407
|
+
index = int(full_path[-1].lstrip("[").rstrip("]"))
|
|
408
|
+
# we need to set a placeholder value as the skips are based on index
|
|
409
|
+
# if we are to pop the values, the next skip index will have shifted and won't be correct
|
|
410
|
+
helper[index] = _SKIP_PLACEHOLDER_VALUE
|
|
411
|
+
has_placeholder = True
|
|
412
|
+
except ValueError:
|
|
413
|
+
SNAPSHOT_LOGGER.warning(
|
|
414
|
+
"Snapshot skip path '%s' was not applied as it was invalid for that snapshot",
|
|
415
|
+
path,
|
|
416
|
+
exc_info=SNAPSHOT_LOGGER.isEnabledFor(logging.DEBUG),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if has_placeholder:
|
|
420
|
+
_remove_placeholder(tmp)
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/report.py
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import re
|
|
2
3
|
|
|
3
4
|
from localstack_snapshot.snapshots import SnapshotMatchResult
|
|
4
5
|
|
|
@@ -29,6 +30,8 @@ _esctable = {
|
|
|
29
30
|
"underlined": 4,
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
_regular_json_path_chars_regex = re.compile("[a-zA-Z0-9_-]+")
|
|
34
|
+
|
|
32
35
|
|
|
33
36
|
class PatchPath(str):
|
|
34
37
|
"""
|
|
@@ -52,7 +55,11 @@ def _format_json_path(path: list):
|
|
|
52
55
|
json_str = "$.."
|
|
53
56
|
for idx, elem in enumerate(path):
|
|
54
57
|
if not isinstance(elem, int):
|
|
55
|
-
|
|
58
|
+
_elem = str(elem)
|
|
59
|
+
# we want to wrap in single quotes parts with special characters so that users can copy-paste them directly
|
|
60
|
+
if not _regular_json_path_chars_regex.fullmatch(_elem):
|
|
61
|
+
_elem = f"'{_elem}'"
|
|
62
|
+
json_str += _elem
|
|
56
63
|
if idx < len(path) - 1 and not json_str.endswith(".."):
|
|
57
64
|
json_str += "."
|
|
58
65
|
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/transformer.py
RENAMED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
import json
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
from datetime import datetime
|
|
7
|
+
from json import JSONDecodeError
|
|
6
8
|
from re import Pattern
|
|
7
9
|
from typing import Any, Callable, Optional, Protocol
|
|
8
10
|
|
|
@@ -100,8 +102,14 @@ class ResponseMetaDataTransformer:
|
|
|
100
102
|
if k == "ResponseMetadata":
|
|
101
103
|
metadata = v
|
|
102
104
|
http_headers = metadata.get("HTTPHeaders")
|
|
105
|
+
if not isinstance(http_headers, dict):
|
|
106
|
+
continue
|
|
107
|
+
|
|
103
108
|
# TODO "x-amz-bucket-region"
|
|
104
109
|
# TestS3.test_region_header_exists -> verifies bucket-region
|
|
110
|
+
|
|
111
|
+
# FIXME: proper value is `content-type` with no underscore in lowercase, but this will necessitate a
|
|
112
|
+
# refresh of all snapshots
|
|
105
113
|
headers_to_collect = ["content_type"]
|
|
106
114
|
simplified_headers = {}
|
|
107
115
|
for h in headers_to_collect:
|
|
@@ -184,35 +192,36 @@ class RegexTransformer:
|
|
|
184
192
|
return input_data
|
|
185
193
|
|
|
186
194
|
|
|
187
|
-
class
|
|
195
|
+
class KeyValueBasedTransformerFunctionReplacement:
|
|
188
196
|
def __init__(
|
|
189
197
|
self,
|
|
190
198
|
match_fn: Callable[[str, Any], Optional[str]],
|
|
191
|
-
|
|
199
|
+
replacement_function: [Callable[[str, Any], str]],
|
|
192
200
|
replace_reference: bool = True,
|
|
193
201
|
):
|
|
194
202
|
self.match_fn = match_fn
|
|
195
|
-
self.
|
|
203
|
+
self.replacement_function = replacement_function
|
|
196
204
|
self.replace_reference = replace_reference
|
|
197
205
|
|
|
198
206
|
def transform(self, input_data: dict, *, ctx: TransformContext) -> dict:
|
|
199
207
|
for k, v in input_data.items():
|
|
200
208
|
if (match_result := self.match_fn(k, v)) is not None:
|
|
209
|
+
replacement = self.replacement_function(k, v)
|
|
201
210
|
if self.replace_reference:
|
|
202
211
|
_register_serialized_reference_replacement(
|
|
203
|
-
ctx, reference_value=match_result, replacement=
|
|
212
|
+
ctx, reference_value=match_result, replacement=replacement
|
|
204
213
|
)
|
|
205
214
|
else:
|
|
206
215
|
if isinstance(v, str):
|
|
207
216
|
SNAPSHOT_LOGGER.debug(
|
|
208
|
-
f"Replacing value for key '{k}': Match result '{match_result:.200s}' with '{
|
|
217
|
+
f"Replacing value for key '{k}': Match result '{match_result:.200s}' with '{replacement}'. (Original value: {str(v)})"
|
|
209
218
|
)
|
|
210
|
-
input_data[k] = v.replace(match_result,
|
|
219
|
+
input_data[k] = v.replace(match_result, replacement)
|
|
211
220
|
else:
|
|
212
221
|
SNAPSHOT_LOGGER.debug(
|
|
213
|
-
f"Replacing value for key '{k}' with '{
|
|
222
|
+
f"Replacing value for key '{k}' with '{replacement}'. (Original value: {str(v)})"
|
|
214
223
|
)
|
|
215
|
-
input_data[k] =
|
|
224
|
+
input_data[k] = replacement
|
|
216
225
|
elif isinstance(v, list) and len(v) > 0:
|
|
217
226
|
for i in range(0, len(v)):
|
|
218
227
|
if isinstance(v[i], dict):
|
|
@@ -223,6 +232,20 @@ class KeyValueBasedTransformer:
|
|
|
223
232
|
return input_data
|
|
224
233
|
|
|
225
234
|
|
|
235
|
+
class KeyValueBasedTransformer(KeyValueBasedTransformerFunctionReplacement):
|
|
236
|
+
def __init__(
|
|
237
|
+
self,
|
|
238
|
+
match_fn: Callable[[str, Any], Optional[str]],
|
|
239
|
+
replacement: str,
|
|
240
|
+
replace_reference: bool = True,
|
|
241
|
+
):
|
|
242
|
+
super().__init__(
|
|
243
|
+
match_fn=match_fn,
|
|
244
|
+
replacement_function=lambda k, v: replacement,
|
|
245
|
+
replace_reference=replace_reference,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
226
249
|
class GenericTransformer:
|
|
227
250
|
def __init__(self, fn: Callable[[dict, TransformContext], dict]):
|
|
228
251
|
self.fn = fn
|
|
@@ -360,3 +383,78 @@ class TextTransformer:
|
|
|
360
383
|
f"Registering text pattern '{self.text}' in snapshot with '{self.replacement}'"
|
|
361
384
|
)
|
|
362
385
|
return input_data
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class JsonStringTransformer:
|
|
389
|
+
"""
|
|
390
|
+
Parses JSON string at the specified key.
|
|
391
|
+
Additionally, attempts to parse any JSON strings inside the parsed JSON
|
|
392
|
+
|
|
393
|
+
This transformer complements the default parsing of JSON strings in
|
|
394
|
+
localstack_snapshot.snapshots.prototype.SnapshotSession._transform_dict_to_parseable_values
|
|
395
|
+
|
|
396
|
+
Shortcomings of the default parser that this transformer addresses:
|
|
397
|
+
- parsing of nested JSON strings '{"a": "{\\"b\\":42}"}'
|
|
398
|
+
- parsing of JSON arrays at the specified key, e.g. '["a", "b"]'
|
|
399
|
+
|
|
400
|
+
Such parsing allows applying transformations further to the elements of the parsed JSON - timestamps, ARNs, etc.
|
|
401
|
+
|
|
402
|
+
Such parsing is not done by default because it's not a common use case.
|
|
403
|
+
Whether to parse a JSON string or not should be decided by the user on a case by case basis.
|
|
404
|
+
Limited general parsing that we already have is preserved for backwards compatibility.
|
|
405
|
+
"""
|
|
406
|
+
|
|
407
|
+
key: str
|
|
408
|
+
|
|
409
|
+
def __init__(self, key: str):
|
|
410
|
+
self.key = key
|
|
411
|
+
|
|
412
|
+
def transform(self, input_data: dict, *, ctx: TransformContext = None) -> dict:
|
|
413
|
+
return self._transform_dict(input_data, ctx=ctx)
|
|
414
|
+
|
|
415
|
+
def _transform(self, input_data: Any, ctx: TransformContext = None) -> Any:
|
|
416
|
+
if isinstance(input_data, dict):
|
|
417
|
+
return self._transform_dict(input_data, ctx=ctx)
|
|
418
|
+
elif isinstance(input_data, list):
|
|
419
|
+
return self._transform_list(input_data, ctx=ctx)
|
|
420
|
+
return input_data
|
|
421
|
+
|
|
422
|
+
def _transform_dict(self, input_data: dict, ctx: TransformContext = None) -> dict:
|
|
423
|
+
for k, v in input_data.items():
|
|
424
|
+
if k == self.key and isinstance(v, str) and v.strip().startswith(("{", "[")):
|
|
425
|
+
try:
|
|
426
|
+
SNAPSHOT_LOGGER.debug(f"Replacing string value of {k} with parsed JSON")
|
|
427
|
+
json_value = json.loads(v)
|
|
428
|
+
input_data[k] = self._transform_nested(json_value)
|
|
429
|
+
except JSONDecodeError:
|
|
430
|
+
SNAPSHOT_LOGGER.exception(
|
|
431
|
+
f'Value mapped to "{k}" key is not a valid JSON string and won\'t be transformed. Value: {v}'
|
|
432
|
+
)
|
|
433
|
+
else:
|
|
434
|
+
input_data[k] = self._transform(v, ctx=ctx)
|
|
435
|
+
return input_data
|
|
436
|
+
|
|
437
|
+
def _transform_list(self, input_data: list, ctx: TransformContext = None) -> list:
|
|
438
|
+
return [self._transform(item, ctx=ctx) for item in input_data]
|
|
439
|
+
|
|
440
|
+
def _transform_nested(self, input_data: Any) -> Any:
|
|
441
|
+
"""
|
|
442
|
+
Separate method from the main `_transform_dict` one because
|
|
443
|
+
it checks every string while the main one attempts to load at specified key only.
|
|
444
|
+
This one is implicit, best-effort attempt,
|
|
445
|
+
while the main one is explicit about at which key transform should happen
|
|
446
|
+
"""
|
|
447
|
+
if isinstance(input_data, list):
|
|
448
|
+
input_data = [self._transform_nested(item) for item in input_data]
|
|
449
|
+
if isinstance(input_data, dict):
|
|
450
|
+
for k, v in input_data.items():
|
|
451
|
+
input_data[k] = self._transform_nested(v)
|
|
452
|
+
if isinstance(input_data, str) and input_data.strip().startswith(("{", "[")):
|
|
453
|
+
try:
|
|
454
|
+
json_value = json.loads(input_data)
|
|
455
|
+
input_data = self._transform_nested(json_value)
|
|
456
|
+
except JSONDecodeError:
|
|
457
|
+
SNAPSHOT_LOGGER.debug(
|
|
458
|
+
f"The value is not a valid JSON string and won't be transformed. The value: {input_data}"
|
|
459
|
+
)
|
|
460
|
+
return input_data
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from re import Pattern
|
|
2
|
-
from typing import Optional
|
|
2
|
+
from typing import Any, Callable, Optional
|
|
3
3
|
|
|
4
4
|
from localstack_snapshot.snapshots.transformer import (
|
|
5
5
|
JsonpathTransformer,
|
|
6
|
+
JsonStringTransformer,
|
|
6
7
|
KeyValueBasedTransformer,
|
|
8
|
+
KeyValueBasedTransformerFunctionReplacement,
|
|
7
9
|
RegexTransformer,
|
|
10
|
+
SortingTransformer,
|
|
8
11
|
TextTransformer,
|
|
9
12
|
)
|
|
10
13
|
|
|
@@ -38,6 +41,33 @@ class TransformerUtility:
|
|
|
38
41
|
replace_reference=reference_replacement,
|
|
39
42
|
)
|
|
40
43
|
|
|
44
|
+
@staticmethod
|
|
45
|
+
def key_value_replacement_function(
|
|
46
|
+
key: str,
|
|
47
|
+
replacement_function: Callable[[str, Any], str] = None,
|
|
48
|
+
reference_replacement: bool = True,
|
|
49
|
+
):
|
|
50
|
+
"""Creates a new KeyValueBasedTransformer. If the key matches, the value will be replaced.
|
|
51
|
+
|
|
52
|
+
:param key: the name of the key which should be replaced
|
|
53
|
+
:param replacement_function: The function calculating the replacement. Will be passed the key and value of the replaced pair.
|
|
54
|
+
By default it is the key-name in lowercase, separated with hyphen
|
|
55
|
+
:param reference_replacement: if False, only the original value for this key will be replaced.
|
|
56
|
+
If True all references of this value will be replaced (using a regex pattern), for the entire test case.
|
|
57
|
+
In this case, the replaced value will be nummerated as well.
|
|
58
|
+
Default: True
|
|
59
|
+
|
|
60
|
+
:return: KeyValueBasedTransformer
|
|
61
|
+
"""
|
|
62
|
+
replacement_function = replacement_function or (
|
|
63
|
+
lambda x, y: _replace_camel_string_with_hyphen(key)
|
|
64
|
+
)
|
|
65
|
+
return KeyValueBasedTransformerFunctionReplacement(
|
|
66
|
+
lambda k, v: v if k == key and (v is not None and v != "") else None,
|
|
67
|
+
replacement_function=replacement_function,
|
|
68
|
+
replace_reference=reference_replacement,
|
|
69
|
+
)
|
|
70
|
+
|
|
41
71
|
@staticmethod
|
|
42
72
|
def jsonpath(jsonpath: str, value_replacement: str, reference_replacement: bool = True):
|
|
43
73
|
"""Creates a new JsonpathTransformer. If the jsonpath matches, the value will be replaced.
|
|
@@ -81,3 +111,27 @@ class TransformerUtility:
|
|
|
81
111
|
:return: TextTransformer
|
|
82
112
|
"""
|
|
83
113
|
return TextTransformer(text, replacement)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def json_string(key: str) -> JsonStringTransformer:
|
|
117
|
+
"""Creates a new JsonStringTransformer. If there is a valid JSON text string at specified key
|
|
118
|
+
it will be loaded as a regular object or array.
|
|
119
|
+
|
|
120
|
+
:param key: key at which JSON string is expected
|
|
121
|
+
|
|
122
|
+
:return: JsonStringTransformer
|
|
123
|
+
"""
|
|
124
|
+
return JsonStringTransformer(key)
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def sorting(key: str, sorting_fn: Optional[Callable[[...], Any]]) -> SortingTransformer:
|
|
128
|
+
"""Creates a new SortingTransformer.
|
|
129
|
+
|
|
130
|
+
Sorts a list at `key` with the given `sorting_fn` (argument for `sorted(list, key=sorting_fn)`)
|
|
131
|
+
|
|
132
|
+
:param key: key at which the list to sort is expected
|
|
133
|
+
:param sorting_fn: sorting function
|
|
134
|
+
|
|
135
|
+
:return: SortingTransformer
|
|
136
|
+
"""
|
|
137
|
+
return SortingTransformer(key, sorting_fn)
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: localstack-snapshot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Extracted snapshot testing lib for LocalStack
|
|
5
5
|
Author-email: LocalStack Contributors <info@localstack.cloud>
|
|
6
6
|
License:
|
|
@@ -222,6 +222,7 @@ Requires-Dist: black==23.10.0; extra == "dev"
|
|
|
222
222
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
223
223
|
Requires-Dist: coverage[toml]>=5.0.0; extra == "dev"
|
|
224
224
|
Requires-Dist: ruff==0.1.0; extra == "dev"
|
|
225
|
+
Dynamic: license-file
|
|
225
226
|
|
|
226
227
|
Snapshot testing for pytest
|
|
227
228
|
===============================
|
|
@@ -233,10 +234,19 @@ This project is in a very early stage and will be both restructured and renamed.
|
|
|
233
234
|
|
|
234
235
|
## Quickstart
|
|
235
236
|
|
|
236
|
-
|
|
237
|
+
To install the python and other developer requirements into a venv run:
|
|
237
238
|
|
|
238
239
|
make install
|
|
239
240
|
|
|
241
|
+
### Configuration options
|
|
242
|
+
|
|
243
|
+
There's a few env vars that can be used with this project:
|
|
244
|
+
|
|
245
|
+
* `TEST_TARGET`: Set to `AWS_CLOUD` to use an externally-deployed instance when running tests.
|
|
246
|
+
* `SNAPSHOT_LEGACY_REPORT`: By default set to `0`. Can be set to `1`. This enables the legacy reporting output style.
|
|
247
|
+
* `SNAPSHOT_UDPATE`: By default set to `0`. Can be set to `1`. This enables updating the snapshot file rather than comparing with its contents.
|
|
248
|
+
* `SNAPSHOT_RAW`: By default set to `0`. Can be set to `1`. This outputs an additional snapshot file which contains the untransformed values.
|
|
249
|
+
|
|
240
250
|
## Format code
|
|
241
251
|
|
|
242
252
|
We use black and isort as code style tools.
|
|
@@ -7,7 +7,7 @@ name = "localstack-snapshot"
|
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "LocalStack Contributors", email = "info@localstack.cloud" }
|
|
9
9
|
]
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.3.1"
|
|
11
11
|
description = "Extracted snapshot testing lib for LocalStack"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"jsonpath-ng>1.6",
|
|
@@ -191,6 +191,137 @@ class TestSnapshotManager:
|
|
|
191
191
|
sm.match("key1", [{"key2": "value1"}, "value2", 3])
|
|
192
192
|
sm._assert_all()
|
|
193
193
|
|
|
194
|
+
def test_list_as_last_node_in_skip_verification_path(self):
|
|
195
|
+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
196
|
+
sm.recorded_state = {"key_a": {"aaa": ["item1", "item2", "item3"]}}
|
|
197
|
+
sm.match(
|
|
198
|
+
"key_a",
|
|
199
|
+
{"aaa": ["item1", "different-value"]},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
|
|
203
|
+
sm._assert_all()
|
|
204
|
+
ctx.match("Parity snapshot failed")
|
|
205
|
+
|
|
206
|
+
skip_path = ["$..aaa[1]", "$..aaa[2]"]
|
|
207
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
208
|
+
|
|
209
|
+
skip_path = ["$..aaa.1", "$..aaa.2"]
|
|
210
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
211
|
+
|
|
212
|
+
def test_list_as_last_node_in_skip_verification_path_complex(self):
|
|
213
|
+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
214
|
+
sm.recorded_state = {
|
|
215
|
+
"key_a": {
|
|
216
|
+
"aaa": [
|
|
217
|
+
{"aab": ["aac", "aad"]},
|
|
218
|
+
{"aab": ["aac", "aad"]},
|
|
219
|
+
{"aab": ["aac", "aad"]},
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
sm.match(
|
|
224
|
+
"key_a",
|
|
225
|
+
{
|
|
226
|
+
"aaa": [
|
|
227
|
+
{"aab": ["aac", "bad-value"], "bbb": "value"},
|
|
228
|
+
{"aab": ["aac", "aad", "bad-value"]},
|
|
229
|
+
{"aab": ["bad-value", "aad"]},
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
|
|
235
|
+
sm._assert_all()
|
|
236
|
+
ctx.match("Parity snapshot failed")
|
|
237
|
+
|
|
238
|
+
skip_path = [
|
|
239
|
+
"$..aaa[0].aab[1]",
|
|
240
|
+
"$..aaa[0].bbb",
|
|
241
|
+
"$..aaa[1].aab[2]",
|
|
242
|
+
"$..aaa[2].aab[0]",
|
|
243
|
+
]
|
|
244
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
245
|
+
|
|
246
|
+
skip_path = [
|
|
247
|
+
"$..aaa.0..aab.1",
|
|
248
|
+
"$..aaa.0..bbb",
|
|
249
|
+
"$..aaa.1..aab.2",
|
|
250
|
+
"$..aaa.2..aab.0",
|
|
251
|
+
]
|
|
252
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
253
|
+
|
|
254
|
+
def test_list_as_mid_node_in_skip_verification_path(self):
|
|
255
|
+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
256
|
+
sm.recorded_state = {"key_a": {"aaa": [{"aab": "value1"}, {"aab": "value2"}]}}
|
|
257
|
+
sm.match(
|
|
258
|
+
"key_a",
|
|
259
|
+
{"aaa": [{"aab": "value1"}, {"aab": "bad-value"}]},
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
|
|
263
|
+
sm._assert_all()
|
|
264
|
+
ctx.match("Parity snapshot failed")
|
|
265
|
+
|
|
266
|
+
skip_path = ["$..aaa[1].aab"]
|
|
267
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
268
|
+
|
|
269
|
+
skip_path = ["$..aaa.1.aab"]
|
|
270
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
271
|
+
|
|
272
|
+
def test_list_as_last_node_in_skip_verification_path_nested(self):
|
|
273
|
+
sm = SnapshotSession(scope_key="A", verify=True, base_file_path="", update=False)
|
|
274
|
+
sm.recorded_state = {
|
|
275
|
+
"key_a": {
|
|
276
|
+
"aaa": [
|
|
277
|
+
"bbb",
|
|
278
|
+
"ccc",
|
|
279
|
+
[
|
|
280
|
+
"ddd",
|
|
281
|
+
"eee",
|
|
282
|
+
[
|
|
283
|
+
"fff",
|
|
284
|
+
"ggg",
|
|
285
|
+
],
|
|
286
|
+
],
|
|
287
|
+
]
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
sm.match(
|
|
291
|
+
"key_a",
|
|
292
|
+
{
|
|
293
|
+
"aaa": [
|
|
294
|
+
"bbb",
|
|
295
|
+
"ccc",
|
|
296
|
+
[
|
|
297
|
+
"bad-value",
|
|
298
|
+
"eee",
|
|
299
|
+
[
|
|
300
|
+
"fff",
|
|
301
|
+
"ggg",
|
|
302
|
+
],
|
|
303
|
+
],
|
|
304
|
+
]
|
|
305
|
+
},
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
with pytest.raises(Exception) as ctx: # asserts it fail without skipping
|
|
309
|
+
sm._assert_all()
|
|
310
|
+
ctx.match("Parity snapshot failed")
|
|
311
|
+
|
|
312
|
+
skip_path = ["$..aaa[2][0]"]
|
|
313
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
314
|
+
|
|
315
|
+
skip_path = ["$..aaa.2[0]"]
|
|
316
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
317
|
+
|
|
318
|
+
# these 2 will actually skip almost everything, as they will match every first element of any list inside `aaa`
|
|
319
|
+
skip_path = ["$..aaa..[0]"]
|
|
320
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
321
|
+
|
|
322
|
+
skip_path = ["$..aaa..0"]
|
|
323
|
+
sm._assert_all(skip_verification_paths=skip_path)
|
|
324
|
+
|
|
194
325
|
|
|
195
326
|
def test_json_diff_format():
|
|
196
327
|
path = ["Records", 1]
|
|
@@ -209,6 +340,14 @@ def test_json_diff_format():
|
|
|
209
340
|
assert _format_json_path(path) == '"$.."'
|
|
210
341
|
path = [1, 1, 0, "SomeKey"]
|
|
211
342
|
assert _format_json_path(path) == '"$..SomeKey"'
|
|
343
|
+
path = ["Some:Key"]
|
|
344
|
+
assert _format_json_path(path) == "\"$..'Some:Key'\""
|
|
345
|
+
path = ["Some.Key"]
|
|
346
|
+
assert _format_json_path(path) == "\"$..'Some.Key'\""
|
|
347
|
+
path = ["Some-Key"]
|
|
348
|
+
assert _format_json_path(path) == '"$..Some-Key"'
|
|
349
|
+
path = ["Some0Key"]
|
|
350
|
+
assert _format_json_path(path) == '"$..Some0Key"'
|
|
212
351
|
|
|
213
352
|
|
|
214
353
|
def test_sorting_transformer():
|
|
@@ -4,6 +4,8 @@ import json
|
|
|
4
4
|
import pytest
|
|
5
5
|
|
|
6
6
|
from localstack_snapshot.snapshots.transformer import (
|
|
7
|
+
JsonStringTransformer,
|
|
8
|
+
ResponseMetaDataTransformer,
|
|
7
9
|
SortingTransformer,
|
|
8
10
|
TimestampTransformer,
|
|
9
11
|
TransformContext,
|
|
@@ -50,6 +52,81 @@ class TestTransformer:
|
|
|
50
52
|
|
|
51
53
|
assert json.loads(tmp) == expected_key_value_reference
|
|
52
54
|
|
|
55
|
+
def test_key_value_replacement_custom_function(self):
|
|
56
|
+
input = {
|
|
57
|
+
"hello": "12characters",
|
|
58
|
+
"hello2": "again",
|
|
59
|
+
"path": {
|
|
60
|
+
"to": {
|
|
61
|
+
"anotherkey": "hi",
|
|
62
|
+
"twelvesymbol": {"hello": "twelvesymbol"},
|
|
63
|
+
"fifteen_symbols": {"hello": "fifteen_symbols"},
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
key_value = TransformerUtility.key_value_replacement_function(
|
|
69
|
+
"hello",
|
|
70
|
+
replacement_function=lambda k, v: f"placeholder({len(v)})",
|
|
71
|
+
reference_replacement=False,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
expected_key_value = {
|
|
75
|
+
"hello": "placeholder(12)",
|
|
76
|
+
"hello2": "again",
|
|
77
|
+
"path": {
|
|
78
|
+
"to": {
|
|
79
|
+
"anotherkey": "hi",
|
|
80
|
+
"twelvesymbol": {"hello": "placeholder(12)"},
|
|
81
|
+
"fifteen_symbols": {"hello": "placeholder(15)"},
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ctx = TransformContext()
|
|
87
|
+
assert key_value.transform(input, ctx=ctx) == expected_key_value
|
|
88
|
+
assert ctx.serialized_replacements == []
|
|
89
|
+
|
|
90
|
+
def test_key_value_replacement_custom_function_reference_replacement(self):
|
|
91
|
+
input = {
|
|
92
|
+
"hello": "12characters",
|
|
93
|
+
"hello2": "again",
|
|
94
|
+
"path": {
|
|
95
|
+
"to": {
|
|
96
|
+
"anotherkey": "hi",
|
|
97
|
+
"twelvesymbol": {"hello": "twelvesymbol"},
|
|
98
|
+
"fifteen_symbols": {"hello": "fifteen_symbols"},
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
key_value = TransformerUtility.key_value_replacement_function(
|
|
104
|
+
"hello",
|
|
105
|
+
replacement_function=lambda k, v: f"placeholder({len(v)})",
|
|
106
|
+
reference_replacement=True,
|
|
107
|
+
)
|
|
108
|
+
# replacement counters are per replacement key, so it will start from 1 again.
|
|
109
|
+
expected_key_value_reference = {
|
|
110
|
+
"hello": "<placeholder(12):1>",
|
|
111
|
+
"hello2": "again",
|
|
112
|
+
"path": {
|
|
113
|
+
"to": {
|
|
114
|
+
"anotherkey": "hi",
|
|
115
|
+
"<placeholder(12):2>": {"hello": "<placeholder(12):2>"},
|
|
116
|
+
"<placeholder(15):1>": {"hello": "<placeholder(15):1>"},
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
ctx = TransformContext()
|
|
121
|
+
assert key_value.transform(input, ctx=ctx) == input
|
|
122
|
+
assert len(ctx.serialized_replacements) == 3
|
|
123
|
+
|
|
124
|
+
tmp = json.dumps(input, default=str)
|
|
125
|
+
for sr in ctx.serialized_replacements:
|
|
126
|
+
tmp = sr(tmp)
|
|
127
|
+
|
|
128
|
+
assert json.loads(tmp) == expected_key_value_reference
|
|
129
|
+
|
|
53
130
|
def test_key_value_replacement_with_falsy_value(self):
|
|
54
131
|
input = {
|
|
55
132
|
"hello": "world",
|
|
@@ -236,6 +313,57 @@ class TestTransformer:
|
|
|
236
313
|
output = sr(output)
|
|
237
314
|
assert json.loads(output) == expected
|
|
238
315
|
|
|
316
|
+
@pytest.mark.parametrize(
|
|
317
|
+
"input_value,transformed_value",
|
|
318
|
+
[
|
|
319
|
+
pytest.param('{"a": "b"}', {"a": "b"}, id="simple_json_object"),
|
|
320
|
+
pytest.param('{\n "a": "b"\n}', {"a": "b"}, id="formatted_json_object"),
|
|
321
|
+
pytest.param('\n {"a": "b"}', {"a": "b"}, id="json_with_whitespaces"),
|
|
322
|
+
pytest.param('{"a": 42}malformed', '{"a": 42}malformed', id="malformed_json"),
|
|
323
|
+
pytest.param('["a", "b"]', ["a", "b"], id="simple_json_list"),
|
|
324
|
+
pytest.param('{"a": "{\\"b\\":42}"}', {"a": {"b": 42}}, id="nested_json_object"),
|
|
325
|
+
pytest.param(
|
|
326
|
+
'{"a": "\\n {\\n \\"b\\":42}"}',
|
|
327
|
+
{"a": {"b": 42}},
|
|
328
|
+
id="nested_formatted_json_object_with_whitespaces",
|
|
329
|
+
),
|
|
330
|
+
pytest.param(
|
|
331
|
+
'{"a": "[{\\"b\\":\\"c\\"}]"}', {"a": [{"b": "c"}]}, id="nested_json_list"
|
|
332
|
+
),
|
|
333
|
+
pytest.param(
|
|
334
|
+
'{"a": "{\\"b\\":42malformed}"}',
|
|
335
|
+
{"a": '{"b":42malformed}'},
|
|
336
|
+
id="malformed_nested_json",
|
|
337
|
+
),
|
|
338
|
+
pytest.param("[]", [], id="empty_list"),
|
|
339
|
+
pytest.param("{}", {}, id="empty_object"),
|
|
340
|
+
pytest.param("", "", id="empty_string"),
|
|
341
|
+
],
|
|
342
|
+
)
|
|
343
|
+
def test_json_string(self, input_value, transformed_value):
|
|
344
|
+
key = "key"
|
|
345
|
+
input_data = {key: input_value}
|
|
346
|
+
expected = {key: transformed_value}
|
|
347
|
+
|
|
348
|
+
transformer = JsonStringTransformer(key)
|
|
349
|
+
|
|
350
|
+
ctx = TransformContext()
|
|
351
|
+
output = transformer.transform(input_data, ctx=ctx)
|
|
352
|
+
|
|
353
|
+
assert output == expected
|
|
354
|
+
|
|
355
|
+
def test_json_string_in_a_nested_key(self):
|
|
356
|
+
key = "nested-key-in-an-object-hidden-inside-a-list"
|
|
357
|
+
input_data = {"top-level-key": [{key: '{"a": "b"}'}]}
|
|
358
|
+
expected = {"top-level-key": [{key: {"a": "b"}}]}
|
|
359
|
+
|
|
360
|
+
transformer = JsonStringTransformer(key)
|
|
361
|
+
|
|
362
|
+
ctx = TransformContext()
|
|
363
|
+
output = transformer.transform(input_data, ctx=ctx)
|
|
364
|
+
|
|
365
|
+
assert output == expected
|
|
366
|
+
|
|
239
367
|
|
|
240
368
|
class TestTimestampTransformer:
|
|
241
369
|
def test_generic_timestamp_transformer(self):
|
|
@@ -278,3 +406,81 @@ class TestTimestampTransformer:
|
|
|
278
406
|
ctx = TransformContext()
|
|
279
407
|
output = transformer.transform(input, ctx=ctx)
|
|
280
408
|
assert output == expected
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class TestResponseMetaDataTransformer:
|
|
412
|
+
def test_with_headers(self):
|
|
413
|
+
input_data = {"ResponseMetadata": {"HTTPHeaders": {"header1": "value1"}}}
|
|
414
|
+
|
|
415
|
+
metadata_transformer = ResponseMetaDataTransformer()
|
|
416
|
+
|
|
417
|
+
expected_key_value = {"ResponseMetadata": {"HTTPHeaders": {}}}
|
|
418
|
+
|
|
419
|
+
copied = copy.deepcopy(input_data)
|
|
420
|
+
ctx = TransformContext()
|
|
421
|
+
assert metadata_transformer.transform(copied, ctx=ctx) == expected_key_value
|
|
422
|
+
assert ctx.serialized_replacements == []
|
|
423
|
+
|
|
424
|
+
def test_with_headers_and_status_code(self):
|
|
425
|
+
input_data = {
|
|
426
|
+
"ResponseMetadata": {"HTTPHeaders": {"header1": "value1"}, "HTTPStatusCode": 500}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
metadata_transformer = ResponseMetaDataTransformer()
|
|
430
|
+
|
|
431
|
+
expected_key_value = {"ResponseMetadata": {"HTTPHeaders": {}, "HTTPStatusCode": 500}}
|
|
432
|
+
|
|
433
|
+
copied = copy.deepcopy(input_data)
|
|
434
|
+
ctx = TransformContext()
|
|
435
|
+
assert metadata_transformer.transform(copied, ctx=ctx) == expected_key_value
|
|
436
|
+
assert ctx.serialized_replacements == []
|
|
437
|
+
|
|
438
|
+
def test_with_status_code_only(self):
|
|
439
|
+
input_data = {"ResponseMetadata": {"HTTPStatusCode": 500, "RandomData": "random"}}
|
|
440
|
+
|
|
441
|
+
metadata_transformer = ResponseMetaDataTransformer()
|
|
442
|
+
|
|
443
|
+
expected_key_value = {"ResponseMetadata": {"HTTPStatusCode": 500, "RandomData": "random"}}
|
|
444
|
+
|
|
445
|
+
copied = copy.deepcopy(input_data)
|
|
446
|
+
ctx = TransformContext()
|
|
447
|
+
assert metadata_transformer.transform(copied, ctx=ctx) == expected_key_value
|
|
448
|
+
assert ctx.serialized_replacements == []
|
|
449
|
+
|
|
450
|
+
def test_with_empty_response_metadata(self):
|
|
451
|
+
input_data = {"ResponseMetadata": {"NotHeaders": "data"}}
|
|
452
|
+
|
|
453
|
+
metadata_transformer = ResponseMetaDataTransformer()
|
|
454
|
+
|
|
455
|
+
expected_key_value = {"ResponseMetadata": {"NotHeaders": "data"}}
|
|
456
|
+
|
|
457
|
+
copied = copy.deepcopy(input_data)
|
|
458
|
+
ctx = TransformContext()
|
|
459
|
+
assert metadata_transformer.transform(copied, ctx=ctx) == expected_key_value
|
|
460
|
+
assert ctx.serialized_replacements == []
|
|
461
|
+
|
|
462
|
+
def test_with_headers_wrong_type(self):
|
|
463
|
+
input_data = {"ResponseMetadata": {"HTTPHeaders": "data"}}
|
|
464
|
+
|
|
465
|
+
metadata_transformer = ResponseMetaDataTransformer()
|
|
466
|
+
|
|
467
|
+
expected_key_value = {"ResponseMetadata": {"HTTPHeaders": "data"}}
|
|
468
|
+
|
|
469
|
+
copied = copy.deepcopy(input_data)
|
|
470
|
+
ctx = TransformContext()
|
|
471
|
+
assert metadata_transformer.transform(copied, ctx=ctx) == expected_key_value
|
|
472
|
+
assert ctx.serialized_replacements == []
|
|
473
|
+
|
|
474
|
+
def test_headers_filtering(self):
|
|
475
|
+
input_data = {
|
|
476
|
+
"ResponseMetadata": {"HTTPHeaders": {"content_type": "value1", "header1": "value1"}}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
metadata_transformer = ResponseMetaDataTransformer()
|
|
480
|
+
|
|
481
|
+
expected_key_value = {"ResponseMetadata": {"HTTPHeaders": {"content_type": "value1"}}}
|
|
482
|
+
|
|
483
|
+
copied = copy.deepcopy(input_data)
|
|
484
|
+
ctx = TransformContext()
|
|
485
|
+
assert metadata_transformer.transform(copied, ctx=ctx) == expected_key_value
|
|
486
|
+
assert ctx.serialized_replacements == []
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
Snapshot testing for pytest
|
|
2
|
-
===============================
|
|
3
|
-
|
|
4
|
-
Extracted snapshot testing lib for LocalStack.
|
|
5
|
-
|
|
6
|
-
This project is in a very early stage and will be both restructured and renamed.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## Quickstart
|
|
10
|
-
|
|
11
|
-
to install the python and other developer requirements into a venv run:
|
|
12
|
-
|
|
13
|
-
make install
|
|
14
|
-
|
|
15
|
-
## Format code
|
|
16
|
-
|
|
17
|
-
We use black and isort as code style tools.
|
|
18
|
-
To execute them, run:
|
|
19
|
-
|
|
20
|
-
make format
|
|
21
|
-
|
|
22
|
-
## Build distribution
|
|
23
|
-
|
|
24
|
-
To build a wheel and source distribution, simply run
|
|
25
|
-
|
|
26
|
-
make dist
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.0"
|
|
File without changes
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/pytest/__init__.py
RENAMED
|
File without changes
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/pytest/snapshot.py
RENAMED
|
File without changes
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/snapshots/__init__.py
RENAMED
|
File without changes
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/util/__init__.py
RENAMED
|
File without changes
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot/util/encoding.py
RENAMED
|
File without changes
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/requires.txt
RENAMED
|
File without changes
|
{localstack_snapshot-0.2.0 → localstack_snapshot-0.3.1}/localstack_snapshot.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|