spaceforge 0.0.2__py3-none-any.whl → 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spaceforge/__init__.py +12 -4
- spaceforge/__main__.py +3 -3
- spaceforge/_version.py +0 -1
- spaceforge/_version_scm.py +2 -2
- spaceforge/cls.py +8 -4
- spaceforge/conftest.py +89 -0
- spaceforge/generator.py +92 -40
- spaceforge/plugin.py +105 -12
- spaceforge/runner.py +0 -12
- spaceforge/schema.json +23 -5
- spaceforge/{generator_test.py → test_generator.py} +121 -10
- spaceforge/test_generator_binaries.py +167 -0
- spaceforge/test_generator_core.py +180 -0
- spaceforge/test_generator_hooks.py +90 -0
- spaceforge/test_generator_parameters.py +59 -0
- spaceforge/test_plugin.py +357 -0
- spaceforge/test_plugin_file_operations.py +118 -0
- spaceforge/test_plugin_hooks.py +100 -0
- spaceforge/test_plugin_inheritance.py +102 -0
- spaceforge/{runner_test.py → test_runner.py} +2 -65
- spaceforge/test_runner_cli.py +69 -0
- spaceforge/test_runner_core.py +124 -0
- spaceforge/test_runner_execution.py +169 -0
- spaceforge-0.0.3.dist-info/METADATA +595 -0
- spaceforge-0.0.3.dist-info/RECORD +31 -0
- spaceforge/plugin_test.py +0 -621
- spaceforge-0.0.2.dist-info/METADATA +0 -163
- spaceforge-0.0.2.dist-info/RECORD +0 -20
- /spaceforge/{cls_test.py → test_cls.py} +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.3.dist-info}/WHEEL +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.3.dist-info}/entry_points.txt +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Tests for PluginGenerator parameter handling."""
|
|
2
|
+
|
|
3
|
+
from spaceforge.cls import Parameter
|
|
4
|
+
from spaceforge.generator import PluginGenerator
|
|
5
|
+
from spaceforge.plugin import SpaceforgePlugin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestPluginGeneratorParameters:
|
|
9
|
+
"""Test parameter extraction and processing."""
|
|
10
|
+
|
|
11
|
+
def test_should_extract_parameters_when_defined(self) -> None:
|
|
12
|
+
"""Should extract and return parameter list when plugin defines them."""
|
|
13
|
+
|
|
14
|
+
# Arrange
|
|
15
|
+
class ParameterizedPlugin(SpaceforgePlugin):
|
|
16
|
+
__parameters__ = [
|
|
17
|
+
Parameter(
|
|
18
|
+
name="api_key",
|
|
19
|
+
description="API key for authentication",
|
|
20
|
+
required=True,
|
|
21
|
+
sensitive=True,
|
|
22
|
+
),
|
|
23
|
+
Parameter(
|
|
24
|
+
name="endpoint",
|
|
25
|
+
description="API endpoint URL",
|
|
26
|
+
required=False,
|
|
27
|
+
default="https://api.example.com",
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
generator = PluginGenerator()
|
|
32
|
+
generator.plugin_class = ParameterizedPlugin
|
|
33
|
+
|
|
34
|
+
# Act
|
|
35
|
+
parameters = generator.get_plugin_parameters()
|
|
36
|
+
|
|
37
|
+
# Assert
|
|
38
|
+
assert parameters is not None
|
|
39
|
+
assert len(parameters) == 2
|
|
40
|
+
assert parameters[0].name == "api_key"
|
|
41
|
+
assert parameters[0].sensitive is True
|
|
42
|
+
assert parameters[1].name == "endpoint"
|
|
43
|
+
assert parameters[1].default == "https://api.example.com"
|
|
44
|
+
|
|
45
|
+
def test_should_return_none_when_no_parameters_defined(self) -> None:
|
|
46
|
+
"""Should return None when plugin has no parameters."""
|
|
47
|
+
|
|
48
|
+
# Arrange
|
|
49
|
+
class NoParamsPlugin(SpaceforgePlugin):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
generator = PluginGenerator()
|
|
53
|
+
generator.plugin_class = NoParamsPlugin
|
|
54
|
+
|
|
55
|
+
# Act
|
|
56
|
+
parameters = generator.get_plugin_parameters()
|
|
57
|
+
|
|
58
|
+
# Assert
|
|
59
|
+
assert parameters is None
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Dict
|
|
6
|
+
from unittest.mock import Mock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from spaceforge.plugin import SpaceforgePlugin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestSpaceforgePluginInitialization:
|
|
14
|
+
"""Test SpaceforgePlugin initialization and configuration."""
|
|
15
|
+
|
|
16
|
+
def test_should_initialize_with_defaults_when_no_environment_set(self) -> None:
|
|
17
|
+
"""Should set default values when no environment variables are provided."""
|
|
18
|
+
# Arrange & Act
|
|
19
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
20
|
+
plugin = SpaceforgePlugin()
|
|
21
|
+
|
|
22
|
+
# Assert
|
|
23
|
+
assert plugin._api_token is False
|
|
24
|
+
assert plugin._spacelift_domain is False
|
|
25
|
+
assert plugin._api_enabled is False
|
|
26
|
+
assert plugin._workspace_root == os.getcwd()
|
|
27
|
+
assert isinstance(plugin.logger, logging.Logger)
|
|
28
|
+
|
|
29
|
+
def test_should_enable_api_when_valid_credentials_provided(
|
|
30
|
+
self, mock_env: Dict[str, str]
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Should enable API access when both token and endpoint are provided."""
|
|
33
|
+
# Arrange & Act
|
|
34
|
+
with patch.dict(os.environ, mock_env, clear=True):
|
|
35
|
+
plugin = SpaceforgePlugin()
|
|
36
|
+
|
|
37
|
+
# Assert
|
|
38
|
+
assert plugin._api_token == "test_token"
|
|
39
|
+
assert plugin._spacelift_domain == "https://test.spacelift.io"
|
|
40
|
+
assert plugin._api_enabled is True
|
|
41
|
+
assert plugin._workspace_root == os.getcwd()
|
|
42
|
+
|
|
43
|
+
def test_should_normalize_domain_with_trailing_slash(self) -> None:
|
|
44
|
+
"""Should remove trailing slash from domain URL."""
|
|
45
|
+
# Arrange
|
|
46
|
+
test_env = {
|
|
47
|
+
"SPACELIFT_API_TOKEN": "test_token",
|
|
48
|
+
"TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io/",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Act
|
|
52
|
+
with patch.dict(os.environ, test_env, clear=True):
|
|
53
|
+
plugin = SpaceforgePlugin()
|
|
54
|
+
|
|
55
|
+
# Assert
|
|
56
|
+
assert plugin._spacelift_domain == "https://test.spacelift.io"
|
|
57
|
+
assert plugin._api_enabled is True
|
|
58
|
+
|
|
59
|
+
def test_should_disable_api_when_domain_has_no_https_prefix(self) -> None:
|
|
60
|
+
"""Should disable API when domain doesn't use HTTPS."""
|
|
61
|
+
# Arrange
|
|
62
|
+
test_env = {
|
|
63
|
+
"SPACELIFT_API_TOKEN": "test_token",
|
|
64
|
+
"TF_VAR_spacelift_graphql_endpoint": "test.spacelift.io",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Act
|
|
68
|
+
with patch.dict(os.environ, test_env, clear=True):
|
|
69
|
+
plugin = SpaceforgePlugin()
|
|
70
|
+
|
|
71
|
+
# Assert
|
|
72
|
+
assert plugin._spacelift_domain == "test.spacelift.io"
|
|
73
|
+
assert plugin._api_enabled is False
|
|
74
|
+
|
|
75
|
+
def test_should_disable_api_when_only_token_provided(self) -> None:
|
|
76
|
+
"""Should disable API when only token is provided without domain."""
|
|
77
|
+
# Arrange & Act
|
|
78
|
+
with patch.dict(os.environ, {"SPACELIFT_API_TOKEN": "test_token"}, clear=True):
|
|
79
|
+
plugin = SpaceforgePlugin()
|
|
80
|
+
|
|
81
|
+
# Assert
|
|
82
|
+
assert plugin._api_enabled is False
|
|
83
|
+
|
|
84
|
+
def test_should_disable_api_when_only_domain_provided(self) -> None:
|
|
85
|
+
"""Should disable API when only domain is provided without token."""
|
|
86
|
+
# Arrange & Act
|
|
87
|
+
with patch.dict(
|
|
88
|
+
os.environ,
|
|
89
|
+
{"TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io"},
|
|
90
|
+
clear=True,
|
|
91
|
+
):
|
|
92
|
+
plugin = SpaceforgePlugin()
|
|
93
|
+
|
|
94
|
+
# Assert
|
|
95
|
+
assert plugin._api_enabled is False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestSpaceforgePluginLogging:
|
|
99
|
+
"""Test logging configuration and functionality."""
|
|
100
|
+
|
|
101
|
+
def test_should_configure_logger_with_correct_name_and_level(self) -> None:
|
|
102
|
+
"""Should set up logger with proper name and level."""
|
|
103
|
+
# Arrange & Act
|
|
104
|
+
with patch.dict(os.environ, {}, clear=True):
|
|
105
|
+
plugin = SpaceforgePlugin()
|
|
106
|
+
|
|
107
|
+
# Assert
|
|
108
|
+
assert plugin.logger.name == "spaceforge.SpaceforgePlugin"
|
|
109
|
+
assert len(plugin.logger.handlers) >= 1
|
|
110
|
+
assert plugin.logger.getEffectiveLevel() <= logging.INFO
|
|
111
|
+
|
|
112
|
+
def test_should_enable_debug_logging_when_debug_env_set(self) -> None:
|
|
113
|
+
"""Should set DEBUG level when SPACELIFT_DEBUG environment variable is true."""
|
|
114
|
+
# Arrange
|
|
115
|
+
test_env = {"SPACELIFT_DEBUG": "true"}
|
|
116
|
+
|
|
117
|
+
# Act
|
|
118
|
+
with patch.dict(os.environ, test_env, clear=True):
|
|
119
|
+
plugin = SpaceforgePlugin()
|
|
120
|
+
|
|
121
|
+
# Assert
|
|
122
|
+
assert plugin.logger.level == logging.DEBUG
|
|
123
|
+
|
|
124
|
+
def test_should_include_run_id_in_log_format(self) -> None:
|
|
125
|
+
"""Should include run ID in log message format when available."""
|
|
126
|
+
# Arrange
|
|
127
|
+
test_env = {"TF_VAR_spacelift_run_id": "run-123"}
|
|
128
|
+
|
|
129
|
+
# Act
|
|
130
|
+
with patch.dict(os.environ, test_env, clear=True):
|
|
131
|
+
plugin = SpaceforgePlugin()
|
|
132
|
+
formatter = plugin.logger.handlers[0].formatter
|
|
133
|
+
|
|
134
|
+
record = logging.LogRecord(
|
|
135
|
+
name="test",
|
|
136
|
+
level=logging.INFO,
|
|
137
|
+
pathname="",
|
|
138
|
+
lineno=0,
|
|
139
|
+
msg="test message",
|
|
140
|
+
args=(),
|
|
141
|
+
exc_info=None,
|
|
142
|
+
)
|
|
143
|
+
record.levelname = "INFO"
|
|
144
|
+
|
|
145
|
+
# Assert
|
|
146
|
+
assert formatter is not None
|
|
147
|
+
formatted = formatter.format(record)
|
|
148
|
+
assert "[run-123]" in formatted or "[local]" in formatted
|
|
149
|
+
|
|
150
|
+
def test_logger_color_formatting(self) -> None:
|
|
151
|
+
"""Test color formatting for different log levels."""
|
|
152
|
+
plugin = SpaceforgePlugin()
|
|
153
|
+
formatter = plugin.logger.handlers[0].formatter
|
|
154
|
+
assert formatter is not None
|
|
155
|
+
|
|
156
|
+
# Test different log levels
|
|
157
|
+
levels_to_test = [
|
|
158
|
+
(logging.INFO, "INFO"),
|
|
159
|
+
(logging.DEBUG, "DEBUG"),
|
|
160
|
+
(logging.WARNING, "WARNING"),
|
|
161
|
+
(logging.ERROR, "ERROR"),
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
for level, level_name in levels_to_test:
|
|
165
|
+
record = logging.LogRecord(
|
|
166
|
+
name="test",
|
|
167
|
+
level=level,
|
|
168
|
+
pathname="",
|
|
169
|
+
lineno=0,
|
|
170
|
+
msg="test message",
|
|
171
|
+
args=(),
|
|
172
|
+
exc_info=None,
|
|
173
|
+
)
|
|
174
|
+
record.levelname = level_name
|
|
175
|
+
formatted = formatter.format(record)
|
|
176
|
+
|
|
177
|
+
# Should contain color codes and plugin name
|
|
178
|
+
assert "\033[" in formatted # ANSI color codes
|
|
179
|
+
assert "(SpaceforgePlugin)" in formatted
|
|
180
|
+
assert "test message" in formatted
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Hook tests moved to test_plugin_hooks.py
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class TestSpaceforgePluginCLI:
|
|
187
|
+
"""Test command-line interface execution functionality."""
|
|
188
|
+
|
|
189
|
+
def test_should_execute_cli_command_and_log_output_on_success(self) -> None:
|
|
190
|
+
"""Should run CLI command and log output when execution succeeds."""
|
|
191
|
+
# Arrange
|
|
192
|
+
plugin = SpaceforgePlugin()
|
|
193
|
+
mock_process = Mock()
|
|
194
|
+
mock_process.communicate.return_value = (b"success output\n", None)
|
|
195
|
+
mock_process.returncode = 0
|
|
196
|
+
|
|
197
|
+
# Act
|
|
198
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
199
|
+
with patch.object(plugin.logger, "info") as mock_info:
|
|
200
|
+
mock_popen.return_value = mock_process
|
|
201
|
+
plugin.run_cli("echo", "test")
|
|
202
|
+
|
|
203
|
+
# Assert
|
|
204
|
+
mock_popen.assert_called_once_with(
|
|
205
|
+
("echo", "test"), stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
|
206
|
+
)
|
|
207
|
+
mock_info.assert_called_with("success output")
|
|
208
|
+
|
|
209
|
+
def test_should_log_error_when_cli_command_fails(self) -> None:
|
|
210
|
+
"""Should log error details when CLI command returns non-zero exit code."""
|
|
211
|
+
# Arrange
|
|
212
|
+
plugin = SpaceforgePlugin()
|
|
213
|
+
mock_process = Mock()
|
|
214
|
+
mock_process.communicate.return_value = (None, b"error output\n")
|
|
215
|
+
mock_process.returncode = 1
|
|
216
|
+
|
|
217
|
+
# Act
|
|
218
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
219
|
+
with patch.object(plugin.logger, "error") as mock_error:
|
|
220
|
+
mock_popen.return_value = mock_process
|
|
221
|
+
plugin.run_cli("false")
|
|
222
|
+
|
|
223
|
+
# Assert
|
|
224
|
+
mock_error.assert_any_call("Command failed with return code 1")
|
|
225
|
+
mock_error.assert_any_call("error output")
|
|
226
|
+
|
|
227
|
+
def test_run_cli_with_multiple_args(self) -> None:
|
|
228
|
+
"""Test CLI command with multiple arguments."""
|
|
229
|
+
plugin = SpaceforgePlugin()
|
|
230
|
+
|
|
231
|
+
with patch("subprocess.Popen") as mock_popen:
|
|
232
|
+
mock_process = Mock()
|
|
233
|
+
mock_process.communicate.return_value = (b"", None)
|
|
234
|
+
mock_process.returncode = 0
|
|
235
|
+
mock_popen.return_value = mock_process
|
|
236
|
+
|
|
237
|
+
with patch.object(plugin.logger, "debug") as mock_debug:
|
|
238
|
+
plugin.run_cli("git", "status", "--porcelain")
|
|
239
|
+
|
|
240
|
+
mock_popen.assert_called_once_with(
|
|
241
|
+
("git", "status", "--porcelain"),
|
|
242
|
+
stdout=subprocess.PIPE,
|
|
243
|
+
stderr=subprocess.STDOUT,
|
|
244
|
+
)
|
|
245
|
+
mock_debug.assert_called_with("Running CLI command: git status --porcelain")
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TestSpaceforgePluginAPI:
|
|
249
|
+
"""Test GraphQL API interaction functionality."""
|
|
250
|
+
|
|
251
|
+
def test_should_exit_with_error_when_api_disabled(self) -> None:
|
|
252
|
+
"""Should exit with error message when API is not enabled."""
|
|
253
|
+
# Arrange
|
|
254
|
+
plugin = SpaceforgePlugin()
|
|
255
|
+
plugin._api_enabled = False
|
|
256
|
+
|
|
257
|
+
# Act & Assert
|
|
258
|
+
with patch.object(plugin.logger, "error") as mock_error:
|
|
259
|
+
with pytest.raises(SystemExit):
|
|
260
|
+
plugin.query_api("query { test }")
|
|
261
|
+
|
|
262
|
+
mock_error.assert_called_with(
|
|
263
|
+
'API is not enabled, please export "SPACELIFT_API_TOKEN" and "TF_VAR_spacelift_graphql_endpoint".'
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def test_should_make_successful_api_request_with_correct_format(
|
|
267
|
+
self, mock_api_response: Mock
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Should execute GraphQL query with proper authentication and format."""
|
|
270
|
+
# Arrange
|
|
271
|
+
plugin = SpaceforgePlugin()
|
|
272
|
+
plugin._api_enabled = True
|
|
273
|
+
plugin._api_token = "test_token"
|
|
274
|
+
plugin._spacelift_domain = "https://test.spacelift.io"
|
|
275
|
+
|
|
276
|
+
expected_data = {"data": {"test": "result"}}
|
|
277
|
+
mock_api_response.read.return_value = json.dumps(expected_data).encode("utf-8")
|
|
278
|
+
|
|
279
|
+
# Act
|
|
280
|
+
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
281
|
+
with patch("urllib.request.Request") as mock_request:
|
|
282
|
+
mock_urlopen.return_value.__enter__ = Mock(
|
|
283
|
+
return_value=mock_api_response
|
|
284
|
+
)
|
|
285
|
+
mock_urlopen.return_value.__exit__ = Mock(return_value=None)
|
|
286
|
+
|
|
287
|
+
result = plugin.query_api("query { test }")
|
|
288
|
+
|
|
289
|
+
# Assert
|
|
290
|
+
mock_request.assert_called_once()
|
|
291
|
+
call_args = mock_request.call_args[0]
|
|
292
|
+
assert call_args[0] == "https://test.spacelift.io"
|
|
293
|
+
|
|
294
|
+
request_data = json.loads(call_args[1].decode("utf-8"))
|
|
295
|
+
assert request_data["query"] == "query { test }"
|
|
296
|
+
|
|
297
|
+
headers = mock_request.call_args[0][2]
|
|
298
|
+
assert headers["Content-Type"] == "application/json"
|
|
299
|
+
assert headers["Authorization"] == "Bearer test_token"
|
|
300
|
+
|
|
301
|
+
assert result == expected_data
|
|
302
|
+
|
|
303
|
+
def test_query_api_with_variables(self) -> None:
|
|
304
|
+
"""Test API query with variables."""
|
|
305
|
+
plugin = SpaceforgePlugin()
|
|
306
|
+
plugin._api_enabled = True
|
|
307
|
+
plugin._api_token = "test_token"
|
|
308
|
+
plugin._spacelift_domain = "https://test.spacelift.io"
|
|
309
|
+
|
|
310
|
+
mock_response_data = {"data": {"test": "result"}}
|
|
311
|
+
mock_response = Mock()
|
|
312
|
+
mock_response.read.return_value = json.dumps(mock_response_data).encode("utf-8")
|
|
313
|
+
|
|
314
|
+
variables = {"stackId": "test-stack"}
|
|
315
|
+
|
|
316
|
+
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
317
|
+
with patch("urllib.request.Request") as mock_request:
|
|
318
|
+
mock_urlopen.return_value.__enter__ = Mock(return_value=mock_response)
|
|
319
|
+
mock_urlopen.return_value.__exit__ = Mock(return_value=None)
|
|
320
|
+
|
|
321
|
+
plugin.query_api(
|
|
322
|
+
"query ($stackId: ID!) { stack(id: $stackId) { name } }", variables
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Verify request data includes variables
|
|
326
|
+
request_data = json.loads(mock_request.call_args[0][1].decode("utf-8"))
|
|
327
|
+
assert request_data["variables"] == variables
|
|
328
|
+
|
|
329
|
+
def test_query_api_with_errors(self) -> None:
|
|
330
|
+
"""Test API query that returns errors."""
|
|
331
|
+
plugin = SpaceforgePlugin()
|
|
332
|
+
plugin._api_enabled = True
|
|
333
|
+
plugin._api_token = "test_token"
|
|
334
|
+
plugin._spacelift_domain = "https://test.spacelift.io"
|
|
335
|
+
|
|
336
|
+
mock_response_data = {"errors": [{"message": "Test error"}]}
|
|
337
|
+
mock_response = Mock()
|
|
338
|
+
mock_response.read.return_value = json.dumps(mock_response_data).encode("utf-8")
|
|
339
|
+
|
|
340
|
+
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
341
|
+
with patch.object(plugin.logger, "error") as mock_error:
|
|
342
|
+
mock_urlopen.return_value.__enter__ = Mock(return_value=mock_response)
|
|
343
|
+
mock_urlopen.return_value.__exit__ = Mock(return_value=None)
|
|
344
|
+
|
|
345
|
+
result = plugin.query_api("query { test }")
|
|
346
|
+
|
|
347
|
+
mock_error.assert_called_with("Error: [{'message': 'Test error'}]")
|
|
348
|
+
assert result == mock_response_data
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# File operation tests moved to test_plugin_file_operations.py
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# Inheritance tests moved to test_plugin_inheritance.py
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# Edge case tests moved to test_plugin_inheritance.py
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Tests for SpaceforgePlugin file operation methods."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from spaceforge.plugin import SpaceforgePlugin
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestSpaceforgePluginFileOperations:
|
|
14
|
+
"""Test file reading and parsing functionality."""
|
|
15
|
+
|
|
16
|
+
def test_should_return_plan_data_when_plan_file_exists(self, temp_dir: str) -> None:
|
|
17
|
+
"""Should parse and return plan JSON when file exists and is valid."""
|
|
18
|
+
# Arrange
|
|
19
|
+
plugin = SpaceforgePlugin()
|
|
20
|
+
plugin._workspace_root = temp_dir
|
|
21
|
+
plan_data = {"resource_changes": [{"type": "create"}]}
|
|
22
|
+
plan_path = os.path.join(temp_dir, "spacelift.plan.json")
|
|
23
|
+
|
|
24
|
+
with open(plan_path, "w") as f:
|
|
25
|
+
json.dump(plan_data, f)
|
|
26
|
+
|
|
27
|
+
# Act
|
|
28
|
+
result = plugin.get_plan_json()
|
|
29
|
+
|
|
30
|
+
# Assert
|
|
31
|
+
assert result == plan_data
|
|
32
|
+
|
|
33
|
+
def test_should_return_none_and_log_error_when_plan_file_missing(
|
|
34
|
+
self, temp_dir: str
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Should return None and log error when plan file doesn't exist."""
|
|
37
|
+
# Arrange
|
|
38
|
+
plugin = SpaceforgePlugin()
|
|
39
|
+
plugin._workspace_root = temp_dir
|
|
40
|
+
|
|
41
|
+
# Act
|
|
42
|
+
with patch.object(plugin.logger, "error") as mock_error:
|
|
43
|
+
result = plugin.get_plan_json()
|
|
44
|
+
|
|
45
|
+
# Assert
|
|
46
|
+
assert result is None
|
|
47
|
+
mock_error.assert_called_with("spacelift.plan.json does not exist.")
|
|
48
|
+
|
|
49
|
+
def test_should_raise_json_decode_error_when_plan_file_invalid(
|
|
50
|
+
self, temp_dir: str
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Should raise JSONDecodeError when plan file contains invalid JSON."""
|
|
53
|
+
# Arrange
|
|
54
|
+
plugin = SpaceforgePlugin()
|
|
55
|
+
plugin._workspace_root = temp_dir
|
|
56
|
+
plan_path = os.path.join(temp_dir, "spacelift.plan.json")
|
|
57
|
+
|
|
58
|
+
with open(plan_path, "w") as f:
|
|
59
|
+
f.write("invalid json {")
|
|
60
|
+
|
|
61
|
+
# Act & Assert
|
|
62
|
+
with pytest.raises(json.JSONDecodeError):
|
|
63
|
+
plugin.get_plan_json()
|
|
64
|
+
|
|
65
|
+
def test_should_return_state_data_when_state_file_exists(
|
|
66
|
+
self, temp_dir: str
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Should parse and return state JSON when file exists and is valid."""
|
|
69
|
+
# Arrange
|
|
70
|
+
plugin = SpaceforgePlugin()
|
|
71
|
+
plugin._workspace_root = temp_dir
|
|
72
|
+
state_data: Dict[str, Any] = {"values": {"root_module": {}}}
|
|
73
|
+
state_path = os.path.join(temp_dir, "spacelift.state.before.json")
|
|
74
|
+
|
|
75
|
+
with open(state_path, "w") as f:
|
|
76
|
+
json.dump(state_data, f)
|
|
77
|
+
|
|
78
|
+
# Act
|
|
79
|
+
result = plugin.get_state_before_json()
|
|
80
|
+
|
|
81
|
+
# Assert
|
|
82
|
+
assert result == state_data
|
|
83
|
+
|
|
84
|
+
def test_should_return_none_and_log_error_when_state_file_missing(
|
|
85
|
+
self, temp_dir: str
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Should return None and log error when state file doesn't exist."""
|
|
88
|
+
# Arrange
|
|
89
|
+
plugin = SpaceforgePlugin()
|
|
90
|
+
plugin._workspace_root = temp_dir
|
|
91
|
+
|
|
92
|
+
# Act
|
|
93
|
+
with patch.object(plugin.logger, "error") as mock_error:
|
|
94
|
+
result = plugin.get_state_before_json()
|
|
95
|
+
|
|
96
|
+
# Assert
|
|
97
|
+
assert result is None
|
|
98
|
+
mock_error.assert_called_with("spacelift.state.before.json does not exist.")
|
|
99
|
+
|
|
100
|
+
def test_custom_policy_input(
|
|
101
|
+
self, temp_dir: str, monkeypatch: pytest.MonkeyPatch
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Should return custom policy input when file exists."""
|
|
104
|
+
# Arrange
|
|
105
|
+
monkeypatch.chdir(temp_dir)
|
|
106
|
+
plugin = SpaceforgePlugin()
|
|
107
|
+
plugin._is_local = False
|
|
108
|
+
plugin._workspace_root = temp_dir
|
|
109
|
+
custom_policy_data = {"test": "input"}
|
|
110
|
+
|
|
111
|
+
# Act
|
|
112
|
+
plugin.add_to_policy_input("test", custom_policy_data)
|
|
113
|
+
|
|
114
|
+
with open(temp_dir + "/test.custom.spacelift.json", "r") as f:
|
|
115
|
+
policy_data = json.load(f)
|
|
116
|
+
|
|
117
|
+
# Assert
|
|
118
|
+
assert policy_data == custom_policy_data
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Tests for SpaceforgePlugin hook system."""
|
|
2
|
+
|
|
3
|
+
from spaceforge.plugin import SpaceforgePlugin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestSpaceforgePluginHooks:
|
|
7
|
+
"""Test hook method detection and execution."""
|
|
8
|
+
|
|
9
|
+
def test_should_provide_all_expected_hook_methods(self) -> None:
|
|
10
|
+
"""Should define all standard Spacelift hook methods as callable."""
|
|
11
|
+
# Arrange
|
|
12
|
+
plugin = SpaceforgePlugin()
|
|
13
|
+
expected_hooks = [
|
|
14
|
+
"before_init",
|
|
15
|
+
"after_init",
|
|
16
|
+
"before_plan",
|
|
17
|
+
"after_plan",
|
|
18
|
+
"before_apply",
|
|
19
|
+
"after_apply",
|
|
20
|
+
"before_perform",
|
|
21
|
+
"after_perform",
|
|
22
|
+
"before_destroy",
|
|
23
|
+
"after_destroy",
|
|
24
|
+
"after_run",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
# Act & Assert
|
|
28
|
+
for hook_name in expected_hooks:
|
|
29
|
+
assert hasattr(plugin, hook_name)
|
|
30
|
+
hook_method = getattr(plugin, hook_name)
|
|
31
|
+
assert callable(hook_method)
|
|
32
|
+
# Should be able to call without error (default implementation is pass)
|
|
33
|
+
hook_method()
|
|
34
|
+
|
|
35
|
+
def test_should_return_all_available_hooks_for_base_class(self) -> None:
|
|
36
|
+
"""Should detect and return all hook methods defined in base class."""
|
|
37
|
+
# Arrange
|
|
38
|
+
plugin = SpaceforgePlugin()
|
|
39
|
+
expected_hooks = [
|
|
40
|
+
"before_init",
|
|
41
|
+
"after_init",
|
|
42
|
+
"before_plan",
|
|
43
|
+
"after_plan",
|
|
44
|
+
"before_apply",
|
|
45
|
+
"after_apply",
|
|
46
|
+
"before_perform",
|
|
47
|
+
"after_perform",
|
|
48
|
+
"before_destroy",
|
|
49
|
+
"after_destroy",
|
|
50
|
+
"after_run",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Act
|
|
54
|
+
hooks = plugin.get_available_hooks()
|
|
55
|
+
|
|
56
|
+
# Assert
|
|
57
|
+
for expected_hook in expected_hooks:
|
|
58
|
+
assert expected_hook in hooks
|
|
59
|
+
|
|
60
|
+
def test_should_detect_overridden_hooks_in_custom_plugin(self) -> None:
|
|
61
|
+
"""Should include all hooks including overridden ones in custom plugins."""
|
|
62
|
+
|
|
63
|
+
# Arrange
|
|
64
|
+
class TestPluginWithHooks(SpaceforgePlugin):
|
|
65
|
+
def after_plan(self) -> None:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def before_apply(self) -> None:
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def custom_method(self) -> None: # Not a hook
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
plugin = TestPluginWithHooks()
|
|
75
|
+
|
|
76
|
+
# Act
|
|
77
|
+
hooks = plugin.get_available_hooks()
|
|
78
|
+
|
|
79
|
+
# Assert
|
|
80
|
+
assert "after_plan" in hooks
|
|
81
|
+
assert "before_apply" in hooks
|
|
82
|
+
assert "custom_method" not in hooks # Not a recognized hook
|
|
83
|
+
|
|
84
|
+
# Should still have all the expected hooks from the base class
|
|
85
|
+
expected_hooks = [
|
|
86
|
+
"before_init",
|
|
87
|
+
"after_init",
|
|
88
|
+
"before_plan",
|
|
89
|
+
"after_plan",
|
|
90
|
+
"before_apply",
|
|
91
|
+
"after_apply",
|
|
92
|
+
"before_perform",
|
|
93
|
+
"after_perform",
|
|
94
|
+
"before_destroy",
|
|
95
|
+
"after_destroy",
|
|
96
|
+
"after_run",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
for expected_hook in expected_hooks:
|
|
100
|
+
assert expected_hook in hooks
|