oas-patch 0.0.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.
- oas_patch-0.0.1/LICENSE +21 -0
- oas_patch-0.0.1/PKG-INFO +22 -0
- oas_patch-0.0.1/README.md +60 -0
- oas_patch-0.0.1/setup.cfg +4 -0
- oas_patch-0.0.1/setup.py +27 -0
- oas_patch-0.0.1/src/oas_patch.egg-info/PKG-INFO +22 -0
- oas_patch-0.0.1/src/oas_patch.egg-info/SOURCES.txt +15 -0
- oas_patch-0.0.1/src/oas_patch.egg-info/dependency_links.txt +1 -0
- oas_patch-0.0.1/src/oas_patch.egg-info/entry_points.txt +2 -0
- oas_patch-0.0.1/src/oas_patch.egg-info/requires.txt +2 -0
- oas_patch-0.0.1/src/oas_patch.egg-info/top_level.txt +1 -0
- oas_patch-0.0.1/tests/test_file_utils.py +133 -0
- oas_patch-0.0.1/tests/test_integration_complex.py +43 -0
- oas_patch-0.0.1/tests/test_integration_medium.py +73 -0
- oas_patch-0.0.1/tests/test_integration_simple.py +61 -0
- oas_patch-0.0.1/tests/test_oas_patcher_cli.py +108 -0
- oas_patch-0.0.1/tests/test_overlay.py +242 -0
oas_patch-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Matthieu Croissant
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
oas_patch-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oas_patch
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A tool to apply overlays to OpenAPI documents.
|
|
5
|
+
Home-page: https://github.com/mcroissant/oas_patcher
|
|
6
|
+
Author: Matthieu Croissant
|
|
7
|
+
Author-email: your.email@example.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: PyYAML>=6.0
|
|
14
|
+
Requires-Dist: jsonpath-ng>=1.7.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# OpenAPI Specification Patcher
|
|
2
|
+
|
|
3
|
+
The OpenAPI Specification Patcher (oas-patch) is a command-line utility that allows you to programmatically modify or update OpenAPI specifications using [overlay documents](https://github.com/OAI/Overlay-Specification) . It supports both YAML and JSON formats and provides powerful JSONPath-based targeting for updates and removals.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **JSONPath Support**: Use JSONPath expressions to target specific parts of your OpenAPI document.
|
|
7
|
+
- **Flexible Updates**: Apply updates or remove elements from your OpenAPI specification.
|
|
8
|
+
- **YAML and JSON Support**: Works seamlessly with both YAML and JSON OpenAPI documents.
|
|
9
|
+
- **Sanitization**: Optionally remove special characters from your OpenAPI document.
|
|
10
|
+
|
|
11
|
+
## Prerequisites
|
|
12
|
+
- Python 3.7 or higher
|
|
13
|
+
- `pip` for managing Python packages
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
### Install from Source
|
|
17
|
+
Clone the repository and install the tool locally:
|
|
18
|
+
```bash
|
|
19
|
+
git clone https://github.com/your-username/oas-patch
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
The tool provides a simple CLI for applying overlays to OpenAPI documents.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Example Usage
|
|
28
|
+
Apply an overlay to an OpenAPI document and save the result:
|
|
29
|
+
```bash
|
|
30
|
+
oas-patch --openapi openapi.yaml --overlay overlay.yaml --output modified_openapi.yaml
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Apply an overlay and print the result to the console:
|
|
34
|
+
```bash
|
|
35
|
+
oas-patch --openapi openapi.json --overlay overlay.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Sanitize the OpenAPI document while applying the overlay:
|
|
39
|
+
```bash
|
|
40
|
+
oas-patch --openapi openapi.yaml --overlay overlay.yaml --sanitize
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Contributing
|
|
44
|
+
Contributions are welcome! To contribute:
|
|
45
|
+
1. Fork the repository.
|
|
46
|
+
2. Create a new branch for your feature or bugfix.
|
|
47
|
+
3. Submit a pull request with a clear description of your changes.
|
|
48
|
+
|
|
49
|
+
### Running Tests
|
|
50
|
+
To run the tests, install the development dependencies and execute the test suite:
|
|
51
|
+
```bash
|
|
52
|
+
pip install -r requirements-dev.txt
|
|
53
|
+
pytest
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
58
|
+
|
|
59
|
+
## Acknowledgments
|
|
60
|
+
This tool was inspired by the need to programmatically manage OpenAPI specifications in a flexible and reusable way. Special thanks to the open-source community for their contributions to JSONPath and YAML parsing libraries.
|
oas_patch-0.0.1/setup.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name='oas_patch',
|
|
5
|
+
version='0.0.1',
|
|
6
|
+
description='A tool to apply overlays to OpenAPI documents.',
|
|
7
|
+
author='Matthieu Croissant',
|
|
8
|
+
author_email='your.email@example.com',
|
|
9
|
+
url='https://github.com/mcroissant/oas_patcher',
|
|
10
|
+
packages=find_packages(where='src'),
|
|
11
|
+
package_dir={'': 'src'},
|
|
12
|
+
install_requires=[
|
|
13
|
+
'PyYAML>=6.0',
|
|
14
|
+
'jsonpath-ng>=1.7.0'
|
|
15
|
+
],
|
|
16
|
+
entry_points={
|
|
17
|
+
'console_scripts': [
|
|
18
|
+
'oas-patch=oas_patcher_cli:cli', # Update this to reference the correct module
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
classifiers=[
|
|
22
|
+
'Programming Language :: Python :: 3',
|
|
23
|
+
'License :: OSI Approved :: MIT License',
|
|
24
|
+
'Operating System :: OS Independent',
|
|
25
|
+
],
|
|
26
|
+
python_requires='>=3.7',
|
|
27
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oas_patch
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A tool to apply overlays to OpenAPI documents.
|
|
5
|
+
Home-page: https://github.com/mcroissant/oas_patcher
|
|
6
|
+
Author: Matthieu Croissant
|
|
7
|
+
Author-email: your.email@example.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: PyYAML>=6.0
|
|
14
|
+
Requires-Dist: jsonpath-ng>=1.7.0
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: classifier
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
src/oas_patch.egg-info/PKG-INFO
|
|
5
|
+
src/oas_patch.egg-info/SOURCES.txt
|
|
6
|
+
src/oas_patch.egg-info/dependency_links.txt
|
|
7
|
+
src/oas_patch.egg-info/entry_points.txt
|
|
8
|
+
src/oas_patch.egg-info/requires.txt
|
|
9
|
+
src/oas_patch.egg-info/top_level.txt
|
|
10
|
+
tests/test_file_utils.py
|
|
11
|
+
tests/test_integration_complex.py
|
|
12
|
+
tests/test_integration_medium.py
|
|
13
|
+
tests/test_integration_simple.py
|
|
14
|
+
tests/test_oas_patcher_cli.py
|
|
15
|
+
tests/test_overlay.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import mock_open, patch
|
|
3
|
+
from src.file_utils import (
|
|
4
|
+
load_yaml, load_json, load_file,
|
|
5
|
+
save_yaml, save_json, save_file,
|
|
6
|
+
sanitize_content
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_load_yaml_valid():
|
|
11
|
+
"""Test loading a valid YAML file."""
|
|
12
|
+
yaml_content = "key: value"
|
|
13
|
+
with patch("builtins.open", mock_open(read_data=yaml_content)):
|
|
14
|
+
result = load_yaml("test.yaml")
|
|
15
|
+
assert result == {"key": "value"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_load_yaml_invalid():
|
|
19
|
+
"""Test loading an invalid YAML file."""
|
|
20
|
+
invalid_yaml = "key: value: another"
|
|
21
|
+
with patch("builtins.open", mock_open(read_data=invalid_yaml)):
|
|
22
|
+
with pytest.raises(ValueError, match="Invalid YAML format"):
|
|
23
|
+
load_yaml("test.yaml")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_load_yaml_file_not_found():
|
|
27
|
+
"""Test loading a non-existent YAML file."""
|
|
28
|
+
with patch("builtins.open", side_effect=FileNotFoundError):
|
|
29
|
+
with pytest.raises(FileNotFoundError, match="File not found"):
|
|
30
|
+
load_yaml("missing.yaml")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_load_json_valid():
|
|
34
|
+
"""Test loading a valid JSON file."""
|
|
35
|
+
json_content = '{"key": "value"}'
|
|
36
|
+
with patch("builtins.open", mock_open(read_data=json_content)):
|
|
37
|
+
result = load_json("test.json")
|
|
38
|
+
assert result == {"key": "value"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_load_json_invalid():
|
|
42
|
+
"""Test loading an invalid JSON file."""
|
|
43
|
+
invalid_json = '{"key": "value"'
|
|
44
|
+
with patch("builtins.open", mock_open(read_data=invalid_json)):
|
|
45
|
+
with pytest.raises(ValueError, match="Invalid JSON format"):
|
|
46
|
+
load_json("test.json")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_load_json_file_not_found():
|
|
50
|
+
"""Test loading a non-existent JSON file."""
|
|
51
|
+
with patch("builtins.open", side_effect=FileNotFoundError):
|
|
52
|
+
with pytest.raises(FileNotFoundError, match="File not found"):
|
|
53
|
+
load_json("missing.json")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_load_file_yaml():
|
|
57
|
+
"""Test loading a YAML file using load_file."""
|
|
58
|
+
yaml_content = "key: value"
|
|
59
|
+
with patch("builtins.open", mock_open(read_data=yaml_content)):
|
|
60
|
+
result = load_file("test.yaml")
|
|
61
|
+
assert result == {"key": "value"}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_load_file_json():
|
|
65
|
+
"""Test loading a JSON file using load_file."""
|
|
66
|
+
json_content = '{"key": "value"}'
|
|
67
|
+
with patch("builtins.open", mock_open(read_data=json_content)):
|
|
68
|
+
result = load_file("test.json")
|
|
69
|
+
assert result == {"key": "value"}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_load_file_unsupported_format():
|
|
73
|
+
"""Test loading a file with an unsupported format."""
|
|
74
|
+
with pytest.raises(ValueError, match="Unsupported file format"):
|
|
75
|
+
load_file("test.txt")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_save_yaml():
|
|
79
|
+
"""Test saving data to a YAML file."""
|
|
80
|
+
data = {"key": "value"}
|
|
81
|
+
with patch("builtins.open", mock_open()) as mocked_file:
|
|
82
|
+
save_yaml(data, "test.yaml")
|
|
83
|
+
mocked_file.assert_called_once_with("test.yaml", "w", encoding="utf-8")
|
|
84
|
+
# Combine all write calls into a single string and verify the content
|
|
85
|
+
written_content = "".join(call.args[0] for call in mocked_file().write.call_args_list)
|
|
86
|
+
assert written_content == "key: value\n"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_save_json():
|
|
90
|
+
"""Test saving data to a JSON file."""
|
|
91
|
+
data = {"key": "value"}
|
|
92
|
+
with patch("builtins.open", mock_open()) as mocked_file:
|
|
93
|
+
save_json(data, "test.json")
|
|
94
|
+
mocked_file.assert_called_once_with("test.json", "w", encoding="utf-8")
|
|
95
|
+
# Combine all write calls into a single string and verify the content
|
|
96
|
+
written_content = "".join(call.args[0] for call in mocked_file().write.call_args_list)
|
|
97
|
+
assert written_content == '{\n "key": "value"\n}'
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_save_file_yaml():
|
|
101
|
+
"""Test saving data to a YAML file using save_file."""
|
|
102
|
+
data = {"key": "value"}
|
|
103
|
+
with patch("builtins.open", mock_open()) as mocked_file:
|
|
104
|
+
save_file(data, "test.yaml")
|
|
105
|
+
mocked_file.assert_called_once_with("test.yaml", "w", encoding="utf-8")
|
|
106
|
+
# Combine all write calls into a single string and verify the content
|
|
107
|
+
written_content = "".join(call.args[0] for call in mocked_file().write.call_args_list)
|
|
108
|
+
assert written_content == "key: value\n"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_save_file_json():
|
|
112
|
+
"""Test saving data to a JSON file using save_file."""
|
|
113
|
+
data = {"key": "value"}
|
|
114
|
+
with patch("builtins.open", mock_open()) as mocked_file:
|
|
115
|
+
save_file(data, "test.json")
|
|
116
|
+
mocked_file.assert_called_once_with("test.json", "w", encoding="utf-8")
|
|
117
|
+
# Combine all write calls into a single string and verify the content
|
|
118
|
+
written_content = "".join(call.args[0] for call in mocked_file().write.call_args_list)
|
|
119
|
+
assert written_content == '{\n "key": "value"\n}'
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_save_file_unsupported_format():
|
|
123
|
+
"""Test saving data to an unsupported file format."""
|
|
124
|
+
data = {"key": "value"}
|
|
125
|
+
with pytest.raises(ValueError, match="Unsupported file format"):
|
|
126
|
+
save_file(data, "test.txt")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_sanitize_content():
|
|
130
|
+
"""Test removing non-printable characters from a string."""
|
|
131
|
+
content = "Valid\x00 content\x1F with invalid\x7F characters"
|
|
132
|
+
sanitized = sanitize_content(content)
|
|
133
|
+
assert sanitized == "Valid content with invalid characters"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import yaml
|
|
4
|
+
import pytest
|
|
5
|
+
from src.oas_patcher_cli import cli
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("test_case", [
|
|
10
|
+
{
|
|
11
|
+
"name": "petstore",
|
|
12
|
+
"openapi_file": "tests/samples/complex/petstore/openapi.yaml",
|
|
13
|
+
"overlay_file": "tests/samples/complex/petstore/overlay.yaml",
|
|
14
|
+
"expected_file": "tests/samples/complex/petstore/output.yaml",
|
|
15
|
+
}
|
|
16
|
+
])
|
|
17
|
+
def test_integration_file_based(test_case, capsys):
|
|
18
|
+
"""Test the CLI using input and expected output files."""
|
|
19
|
+
with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as temp_output:
|
|
20
|
+
temp_output.close()
|
|
21
|
+
|
|
22
|
+
# Mock CLI arguments
|
|
23
|
+
with patch('sys.argv', [
|
|
24
|
+
'oas-patch',
|
|
25
|
+
'--openapi', test_case["openapi_file"],
|
|
26
|
+
'--overlay', test_case["overlay_file"],
|
|
27
|
+
'--output', temp_output.name
|
|
28
|
+
]):
|
|
29
|
+
cli()
|
|
30
|
+
|
|
31
|
+
# Load the CLI output
|
|
32
|
+
with open(temp_output.name, 'r', encoding='utf-8') as output_file:
|
|
33
|
+
output_data = yaml.safe_load(output_file)
|
|
34
|
+
|
|
35
|
+
# Load the expected output
|
|
36
|
+
with open(test_case["expected_file"], 'r', encoding='utf-8') as expected_file:
|
|
37
|
+
expected_data = yaml.safe_load(expected_file)
|
|
38
|
+
|
|
39
|
+
# Compare the output with the expected data
|
|
40
|
+
assert output_data == expected_data, f"Test case '{test_case['name']}' failed."
|
|
41
|
+
|
|
42
|
+
# Clean up the temporary file
|
|
43
|
+
os.remove(temp_output.name)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import yaml
|
|
4
|
+
import pytest
|
|
5
|
+
from src.oas_patcher_cli import cli
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("test_case", [
|
|
10
|
+
{
|
|
11
|
+
"name": "array_update",
|
|
12
|
+
"openapi_file": "tests/samples/medium/array_update/openapi.yaml",
|
|
13
|
+
"overlay_file": "tests/samples/medium/array_update/overlay.yaml",
|
|
14
|
+
"expected_file": "tests/samples/medium/array_update/output.yaml",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "array_remove",
|
|
18
|
+
"openapi_file": "tests/samples/medium/array_remove/openapi.yaml",
|
|
19
|
+
"overlay_file": "tests/samples/medium/array_remove/overlay.yaml",
|
|
20
|
+
"expected_file": "tests/samples/medium/array_remove/output.yaml",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "remove_update",
|
|
24
|
+
"openapi_file": "tests/samples/medium/remove_update/openapi.yaml",
|
|
25
|
+
"overlay_file": "tests/samples/medium/remove_update/overlay.yaml",
|
|
26
|
+
"expected_file": "tests/samples/medium/remove_update/output.yaml",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "structured_overlay",
|
|
30
|
+
"openapi_file": "tests/samples/medium/structured_overlay/openapi.yaml",
|
|
31
|
+
"overlay_file": "tests/samples/medium/structured_overlay/overlay.yaml",
|
|
32
|
+
"expected_file": "tests/samples/medium/structured_overlay/output.yaml",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "targeted_overlay",
|
|
36
|
+
"openapi_file": "tests/samples/medium/targeted_overlay/openapi.yaml",
|
|
37
|
+
"overlay_file": "tests/samples/medium/targeted_overlay/overlay.yaml",
|
|
38
|
+
"expected_file": "tests/samples/medium/targeted_overlay/output.yaml",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "wildcard_overlay",
|
|
42
|
+
"openapi_file": "tests/samples/medium/wildcard_overlay/openapi.yaml",
|
|
43
|
+
"overlay_file": "tests/samples/medium/wildcard_overlay/overlay.yaml",
|
|
44
|
+
"expected_file": "tests/samples/medium/wildcard_overlay/output.yaml",
|
|
45
|
+
}
|
|
46
|
+
])
|
|
47
|
+
def test_integration_file_based(test_case, capsys):
|
|
48
|
+
"""Test the CLI using input and expected output files."""
|
|
49
|
+
with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as temp_output:
|
|
50
|
+
temp_output.close()
|
|
51
|
+
|
|
52
|
+
# Mock CLI arguments
|
|
53
|
+
with patch('sys.argv', [
|
|
54
|
+
'oas-patch',
|
|
55
|
+
'--openapi', test_case["openapi_file"],
|
|
56
|
+
'--overlay', test_case["overlay_file"],
|
|
57
|
+
'--output', temp_output.name
|
|
58
|
+
]):
|
|
59
|
+
cli()
|
|
60
|
+
|
|
61
|
+
# Load the CLI output
|
|
62
|
+
with open(temp_output.name, 'r', encoding='utf-8') as output_file:
|
|
63
|
+
output_data = yaml.safe_load(output_file)
|
|
64
|
+
|
|
65
|
+
# Load the expected output
|
|
66
|
+
with open(test_case["expected_file"], 'r', encoding='utf-8') as expected_file:
|
|
67
|
+
expected_data = yaml.safe_load(expected_file)
|
|
68
|
+
|
|
69
|
+
# Compare the output with the expected data
|
|
70
|
+
assert output_data == expected_data, f"Test case '{test_case['name']}' failed."
|
|
71
|
+
|
|
72
|
+
# Clean up the temporary file
|
|
73
|
+
os.remove(temp_output.name)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import tempfile
|
|
3
|
+
import yaml
|
|
4
|
+
import pytest
|
|
5
|
+
from src.oas_patcher_cli import cli
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.parametrize("test_case", [
|
|
10
|
+
{
|
|
11
|
+
"name": "update",
|
|
12
|
+
"openapi_file": "tests/samples/simple/update/openapi.yaml",
|
|
13
|
+
"overlay_file": "tests/samples/simple/update/overlay.yaml",
|
|
14
|
+
"expected_file": "tests/samples/simple/update/output.yaml",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "remove",
|
|
18
|
+
"openapi_file": "tests/samples/simple/remove/openapi.yaml",
|
|
19
|
+
"overlay_file": "tests/samples/simple/remove/overlay.yaml",
|
|
20
|
+
"expected_file": "tests/samples/simple/remove/output.yaml",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "multi_action",
|
|
24
|
+
"openapi_file": "tests/samples/simple/multi_action/openapi.yaml",
|
|
25
|
+
"overlay_file": "tests/samples/simple/multi_action/overlay.yaml",
|
|
26
|
+
"expected_file": "tests/samples/simple/multi_action/output.yaml",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "no_match",
|
|
30
|
+
"openapi_file": "tests/samples/simple/no_match/openapi.yaml",
|
|
31
|
+
"overlay_file": "tests/samples/simple/no_match/overlay.yaml",
|
|
32
|
+
"expected_file": "tests/samples/simple/no_match/output.yaml",
|
|
33
|
+
}
|
|
34
|
+
])
|
|
35
|
+
def test_integration_file_based(test_case, capsys):
|
|
36
|
+
"""Test the CLI using input and expected output files."""
|
|
37
|
+
with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as temp_output:
|
|
38
|
+
temp_output.close()
|
|
39
|
+
|
|
40
|
+
# Mock CLI arguments
|
|
41
|
+
with patch('sys.argv', [
|
|
42
|
+
'oas-patch',
|
|
43
|
+
'--openapi', test_case["openapi_file"],
|
|
44
|
+
'--overlay', test_case["overlay_file"],
|
|
45
|
+
'--output', temp_output.name
|
|
46
|
+
]):
|
|
47
|
+
cli()
|
|
48
|
+
|
|
49
|
+
# Load the CLI output
|
|
50
|
+
with open(temp_output.name, 'r', encoding='utf-8') as output_file:
|
|
51
|
+
output_data = yaml.safe_load(output_file)
|
|
52
|
+
|
|
53
|
+
# Load the expected output
|
|
54
|
+
with open(test_case["expected_file"], 'r', encoding='utf-8') as expected_file:
|
|
55
|
+
expected_data = yaml.safe_load(expected_file)
|
|
56
|
+
|
|
57
|
+
# Compare the output with the expected data
|
|
58
|
+
assert output_data == expected_data, f"Test case '{test_case['name']}' failed."
|
|
59
|
+
|
|
60
|
+
# Clean up the temporary file
|
|
61
|
+
os.remove(temp_output.name)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
import yaml
|
|
3
|
+
from src.oas_patcher_cli import cli
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@pytest.fixture
|
|
7
|
+
def mock_load_file(mocker):
|
|
8
|
+
"""Mock the load_file function."""
|
|
9
|
+
return mocker.patch('src.oas_patcher_cli.load_file')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_save_file(mocker):
|
|
14
|
+
"""Mock the save_file function."""
|
|
15
|
+
return mocker.patch('src.oas_patcher_cli.save_file')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_apply_overlay(mocker):
|
|
20
|
+
"""Mock the apply_overlay function."""
|
|
21
|
+
return mocker.patch('src.oas_patcher_cli.apply_overlay')
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def setup_mocks(mock_load_file, mock_apply_overlay):
|
|
26
|
+
"""Set up common mock behavior for load_file and apply_overlay."""
|
|
27
|
+
mock_load_file.side_effect = [
|
|
28
|
+
{"openapi": "3.0.3", "info": {"title": "Sample API"}},
|
|
29
|
+
{"actions": [{"target": "$.info", "update": {"title": "Updated API"}}]},
|
|
30
|
+
]
|
|
31
|
+
mock_apply_overlay.return_value = {"openapi": "3.0.3", "info": {"title": "Updated API"}}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run_cli_with_args(mocker, args):
|
|
35
|
+
"""Helper function to run the CLI with specific arguments."""
|
|
36
|
+
mocker.patch('sys.argv', ['oas-patch'] + args)
|
|
37
|
+
cli()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def assert_load_file_calls(mock_load_file, sanitize=False):
|
|
41
|
+
"""Helper function to assert calls to load_file."""
|
|
42
|
+
mock_load_file.assert_any_call('openapi.yaml', sanitize)
|
|
43
|
+
mock_load_file.assert_any_call('overlay.yaml')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def assert_apply_overlay_call(mock_apply_overlay):
|
|
47
|
+
"""Helper function to assert calls to apply_overlay."""
|
|
48
|
+
mock_apply_overlay.assert_called_once_with(
|
|
49
|
+
{"openapi": "3.0.3", "info": {"title": "Sample API"}},
|
|
50
|
+
{"actions": [{"target": "$.info", "update": {"title": "Updated API"}}]}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def assert_save_file_call(mock_save_file, output_file):
|
|
55
|
+
"""Helper function to assert calls to save_file."""
|
|
56
|
+
mock_save_file.assert_called_once_with(
|
|
57
|
+
{"openapi": "3.0.3", "info": {"title": "Updated API"}}, output_file
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_cli_output_to_file(setup_mocks, mock_save_file, mock_load_file, mock_apply_overlay, mocker):
|
|
62
|
+
"""Test the CLI with output to a file."""
|
|
63
|
+
run_cli_with_args(mocker, ['--openapi', 'openapi.yaml', '--overlay', 'overlay.yaml', '--output', 'output.yaml'])
|
|
64
|
+
|
|
65
|
+
assert_load_file_calls(mock_load_file, sanitize=False)
|
|
66
|
+
assert_apply_overlay_call(mock_apply_overlay)
|
|
67
|
+
assert_save_file_call(mock_save_file, 'output.yaml')
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_cli_output_to_console(setup_mocks, mock_load_file, mock_apply_overlay, mocker, capsys):
|
|
71
|
+
"""Test the CLI with output to the console."""
|
|
72
|
+
run_cli_with_args(mocker, ['--openapi', 'openapi.yaml', '--overlay', 'overlay.yaml'])
|
|
73
|
+
|
|
74
|
+
assert_load_file_calls(mock_load_file, sanitize=False)
|
|
75
|
+
assert_apply_overlay_call(mock_apply_overlay)
|
|
76
|
+
|
|
77
|
+
captured = capsys.readouterr()
|
|
78
|
+
assert yaml.safe_load(captured.out) == {"openapi": "3.0.3", "info": {"title": "Updated API"}}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_cli_missing_required_arguments(mocker):
|
|
82
|
+
"""Test the CLI with missing required arguments."""
|
|
83
|
+
mocker.patch('sys.argv', ['oas-patch'])
|
|
84
|
+
|
|
85
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
86
|
+
cli()
|
|
87
|
+
|
|
88
|
+
assert excinfo.value.code == 2 # argparse exits with code 2 for missing arguments
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_cli_with_sanitize_flag(setup_mocks, mock_load_file, mock_apply_overlay, mocker):
|
|
92
|
+
"""Test the CLI with the --sanitize flag."""
|
|
93
|
+
run_cli_with_args(mocker, ['--openapi', 'openapi.yaml', '--overlay', 'overlay.yaml', '--sanitize'])
|
|
94
|
+
|
|
95
|
+
assert_load_file_calls(mock_load_file, sanitize=True)
|
|
96
|
+
assert_apply_overlay_call(mock_apply_overlay)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_help_command(mocker, capsys):
|
|
100
|
+
"""Test the CLI help message."""
|
|
101
|
+
mocker.patch('sys.argv', ['oas-patch', '--help'])
|
|
102
|
+
|
|
103
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
104
|
+
cli()
|
|
105
|
+
|
|
106
|
+
captured = capsys.readouterr()
|
|
107
|
+
assert "Apply an OpenAPI Overlay to your OpenAPI document." in captured.out
|
|
108
|
+
assert excinfo.value.code == 0 # Ensure the CLI exits with code 0 for help
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from src.overlay import apply_overlay, deep_update
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_apply_overlay_update_action():
|
|
6
|
+
"""Test applying an overlay with update actions."""
|
|
7
|
+
openapi_doc = {
|
|
8
|
+
"paths": {
|
|
9
|
+
"/example": {
|
|
10
|
+
"get": {
|
|
11
|
+
"summary": "Original summary"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
overlay = {
|
|
17
|
+
"actions": [
|
|
18
|
+
{
|
|
19
|
+
"target": "$.paths['/example'].get",
|
|
20
|
+
"update": {
|
|
21
|
+
"summary": "Updated summary"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
result = apply_overlay(openapi_doc, overlay)
|
|
27
|
+
assert result["paths"]["/example"]["get"]["summary"] == "Updated summary"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_apply_overlay_remove_action():
|
|
31
|
+
"""Test applying an overlay with remove actions."""
|
|
32
|
+
openapi_doc = {
|
|
33
|
+
"paths": {
|
|
34
|
+
"/example": {
|
|
35
|
+
"get": {
|
|
36
|
+
"summary": "Original summary"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
overlay = {
|
|
42
|
+
"actions": [
|
|
43
|
+
{
|
|
44
|
+
"target": "$.paths['/example'].get",
|
|
45
|
+
"remove": True
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
result = apply_overlay(openapi_doc, overlay)
|
|
50
|
+
assert "get" not in result["paths"]["/example"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_apply_overlay_no_matching_target():
|
|
54
|
+
"""Test applying an overlay with no matching JSONPath targets."""
|
|
55
|
+
openapi_doc = {
|
|
56
|
+
"paths": {
|
|
57
|
+
"/example": {
|
|
58
|
+
"get": {
|
|
59
|
+
"summary": "Original summary"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
overlay = {
|
|
65
|
+
"actions": [
|
|
66
|
+
{
|
|
67
|
+
"target": "$.paths['/nonexistent'].get",
|
|
68
|
+
"update": {
|
|
69
|
+
"summary": "Updated summary"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
result = apply_overlay(openapi_doc, overlay)
|
|
75
|
+
assert result == openapi_doc # No changes should be made
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_apply_overlay_empty_actions():
|
|
79
|
+
"""Test applying an overlay with an empty actions list."""
|
|
80
|
+
openapi_doc = {
|
|
81
|
+
"paths": {
|
|
82
|
+
"/example": {
|
|
83
|
+
"get": {
|
|
84
|
+
"summary": "Original summary"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
overlay = {
|
|
90
|
+
"actions": []
|
|
91
|
+
}
|
|
92
|
+
result = apply_overlay(openapi_doc, overlay)
|
|
93
|
+
assert result == openapi_doc # No changes should be made
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_deep_update_nested_keys():
|
|
97
|
+
"""Test deep updating a dictionary with nested keys."""
|
|
98
|
+
target = {
|
|
99
|
+
"key1": {
|
|
100
|
+
"subkey1": "value1"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
updates = {
|
|
104
|
+
"key1": {
|
|
105
|
+
"subkey2": "value2"
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
deep_update(target, updates)
|
|
109
|
+
assert target["key1"]["subkey1"] == "value1"
|
|
110
|
+
assert target["key1"]["subkey2"] == "value2"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_deep_update_overwrite_keys():
|
|
114
|
+
"""Test deep updating a dictionary with overwriting keys."""
|
|
115
|
+
target = {
|
|
116
|
+
"key1": {
|
|
117
|
+
"subkey1": "value1"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
updates = {
|
|
121
|
+
"key1": {
|
|
122
|
+
"subkey1": "new_value1"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
deep_update(target, updates)
|
|
126
|
+
assert target["key1"]["subkey1"] == "new_value1"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_deep_update_add_new_keys():
|
|
130
|
+
"""Test deep updating a dictionary by adding new keys."""
|
|
131
|
+
target = {
|
|
132
|
+
"key1": {
|
|
133
|
+
"subkey1": "value1"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
updates = {
|
|
137
|
+
"key2": {
|
|
138
|
+
"subkey2": "value2"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
deep_update(target, updates)
|
|
142
|
+
assert target["key1"]["subkey1"] == "value1"
|
|
143
|
+
assert target["key2"]["subkey2"] == "value2"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_apply_overlay_update_root():
|
|
147
|
+
"""Test applying an overlay with an update action targeting the root."""
|
|
148
|
+
openapi_doc = {
|
|
149
|
+
"openapi": "3.0.3",
|
|
150
|
+
"info": {
|
|
151
|
+
"title": "Sample API",
|
|
152
|
+
"version": "1.0.0"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
overlay = {
|
|
156
|
+
"actions": [
|
|
157
|
+
{
|
|
158
|
+
"target": "$", # Root of the document
|
|
159
|
+
"update": {
|
|
160
|
+
"description": "This is a root-level description"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
result = apply_overlay(openapi_doc, overlay)
|
|
166
|
+
assert result["description"] == "This is a root-level description"
|
|
167
|
+
assert result["openapi"] == "3.0.3" # Ensure existing keys are preserved
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_apply_overlay_remove_root():
|
|
171
|
+
"""Test applying an overlay with a remove action targeting the root."""
|
|
172
|
+
openapi_doc = {
|
|
173
|
+
"openapi": "3.0.3",
|
|
174
|
+
"info": {
|
|
175
|
+
"title": "Sample API",
|
|
176
|
+
"version": "1.0.0"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
overlay = {
|
|
180
|
+
"actions": [
|
|
181
|
+
{
|
|
182
|
+
"target": "$", # Root of the document
|
|
183
|
+
"remove": True
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
with pytest.raises(ValueError, match="Cannot remove the root of the document"):
|
|
188
|
+
apply_overlay(openapi_doc, overlay)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_apply_overlay_non_dict_update_root():
|
|
192
|
+
"""Test applying an overlay with a non-dict update action targeting the root."""
|
|
193
|
+
openapi_doc = {
|
|
194
|
+
"openapi": "3.0.3",
|
|
195
|
+
"info": {
|
|
196
|
+
"title": "Sample API",
|
|
197
|
+
"version": "1.0.0"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
overlay = {
|
|
201
|
+
"actions": [
|
|
202
|
+
{
|
|
203
|
+
"target": "$", # Root of the document
|
|
204
|
+
"update": "Invalid root update"
|
|
205
|
+
}
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
with pytest.raises(ValueError, match="Cannot perform non-dict update on the root of the document"):
|
|
209
|
+
apply_overlay(openapi_doc, overlay)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_apply_overlay_merge_dict_with_array():
|
|
213
|
+
"""Test applying an overlay with a non-dict update action targeting the root."""
|
|
214
|
+
openapi_doc = {
|
|
215
|
+
"openapi": "3.0.3",
|
|
216
|
+
"info": {
|
|
217
|
+
"title": "Sample API",
|
|
218
|
+
"version": "1.0.0"
|
|
219
|
+
},
|
|
220
|
+
"paths": {
|
|
221
|
+
"/example": {
|
|
222
|
+
"get": {
|
|
223
|
+
"summary": "Original summary",
|
|
224
|
+
"security": [
|
|
225
|
+
{
|
|
226
|
+
"api_key": []
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
overlay = {
|
|
234
|
+
"actions": [
|
|
235
|
+
{
|
|
236
|
+
"target": "$.paths.*.get", # Root of the document
|
|
237
|
+
"update": {"security": [{"oauth2": []}]}
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
result = apply_overlay(openapi_doc, overlay)
|
|
242
|
+
assert len(result["paths"]["/example"]["get"]["security"]) == 2 # Ensure both security definitions are present
|