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.
@@ -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.
@@ -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.
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ oas-patch = oas_patcher_cli:cli
@@ -0,0 +1,2 @@
1
+ PyYAML>=6.0
2
+ jsonpath-ng>=1.7.0
@@ -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