spaceforge 0.0.7__tar.gz → 0.0.9__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.
Files changed (58) hide show
  1. {spaceforge-0.0.7 → spaceforge-0.0.9}/PKG-INFO +1 -1
  2. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/enviroment_manager/plugin.py +5 -2
  3. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/enviroment_manager/plugin.yaml +5 -2
  4. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/_version_scm.py +3 -3
  5. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/plugin.py +26 -14
  6. spaceforge-0.0.9/spaceforge/test_plugin.py +357 -0
  7. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge.egg-info/PKG-INFO +1 -1
  8. spaceforge-0.0.7/spaceforge/test_plugin.py +0 -357
  9. {spaceforge-0.0.7 → spaceforge-0.0.9}/.github/workflows/ci.yml +0 -0
  10. {spaceforge-0.0.7 → spaceforge-0.0.9}/.github/workflows/release.yml +0 -0
  11. {spaceforge-0.0.7 → spaceforge-0.0.9}/.gitignore +0 -0
  12. {spaceforge-0.0.7 → spaceforge-0.0.9}/LICENSE +0 -0
  13. {spaceforge-0.0.7 → spaceforge-0.0.9}/MANIFEST.in +0 -0
  14. {spaceforge-0.0.7 → spaceforge-0.0.9}/README.md +0 -0
  15. {spaceforge-0.0.7 → spaceforge-0.0.9}/go.mod +0 -0
  16. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/enviroment_manager/requirements.txt +0 -0
  17. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/infracost/plugin.py +0 -0
  18. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/infracost/plugin.yaml +0 -0
  19. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/sops/plugin.py +0 -0
  20. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/sops/plugin.yaml +0 -0
  21. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/sops/requirements.txt +0 -0
  22. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/wiz/plugin.py +0 -0
  23. {spaceforge-0.0.7 → spaceforge-0.0.9}/plugins/wiz/plugin.yaml +0 -0
  24. {spaceforge-0.0.7 → spaceforge-0.0.9}/pyproject.toml +0 -0
  25. {spaceforge-0.0.7 → spaceforge-0.0.9}/setup.cfg +0 -0
  26. {spaceforge-0.0.7 → spaceforge-0.0.9}/setup.py +0 -0
  27. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/README.md +0 -0
  28. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/__init__.py +0 -0
  29. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/__main__.py +0 -0
  30. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/_version.py +0 -0
  31. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/cls.py +0 -0
  32. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/conftest.py +0 -0
  33. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/generator.py +0 -0
  34. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/runner.py +0 -0
  35. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/schema.json +0 -0
  36. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/templates/binary_install.sh.j2 +0 -0
  37. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +0 -0
  38. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_cls.py +0 -0
  39. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_generator.py +0 -0
  40. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_generator_binaries.py +0 -0
  41. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_generator_core.py +0 -0
  42. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_generator_hooks.py +0 -0
  43. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_generator_parameters.py +0 -0
  44. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_plugin_file_operations.py +0 -0
  45. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_plugin_hooks.py +0 -0
  46. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_plugin_inheritance.py +0 -0
  47. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_runner.py +0 -0
  48. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_runner_cli.py +0 -0
  49. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_runner_core.py +0 -0
  50. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge/test_runner_execution.py +0 -0
  51. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge.egg-info/SOURCES.txt +0 -0
  52. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge.egg-info/dependency_links.txt +0 -0
  53. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge.egg-info/entry_points.txt +0 -0
  54. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge.egg-info/not-zip-safe +0 -0
  55. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge.egg-info/requires.txt +0 -0
  56. {spaceforge-0.0.7 → spaceforge-0.0.9}/spaceforge.egg-info/top_level.txt +0 -0
  57. {spaceforge-0.0.7 → spaceforge-0.0.9}/templates.go +0 -0
  58. {spaceforge-0.0.7 → spaceforge-0.0.9}/test.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spaceforge
3
- Version: 0.0.7
3
+ Version: 0.0.9
4
4
  Summary: A Python framework for building Spacelift plugins
5
5
  Home-page: https://github.com/spacelift-io/plugins
6
6
  Author: Spacelift
@@ -149,6 +149,9 @@ resource "spacelift_environment_variable" "__this" {
149
149
  )
150
150
  ]
151
151
 
152
+ def __init__(self):
153
+ super().__init__()
154
+
152
155
  def load_yaml_file(self, file_path):
153
156
  """Load YAML file and return parsed content"""
154
157
  try:
@@ -190,13 +193,13 @@ resource "spacelift_environment_variable" "__this" {
190
193
  query = "{ stack(id: \"" + stack_id + "\") { trackedCommit { hash } } }"
191
194
  response = self.query_api(query)
192
195
  if "errors" in response:
193
- self.logger.error("Error fetching stack tracked commit:", response["errors"])
196
+ self.logger.error(f"Error fetching stack tracked commit: {response['errors']}")
194
197
  continue
195
198
 
196
199
  # Ensure we have a tracked commit
197
200
  try:
198
201
  tracked_commit = response["data"]["stack"]["trackedCommit"]["hash"]
199
- except TypeError:
202
+ except (TypeError, KeyError):
200
203
  tracked_commit = None
201
204
  if tracked_commit is None:
202
205
  self.logger.error(f"Stack {stack_id} has no tracked commit. Skipping.")
@@ -286,6 +286,9 @@ contexts:
286
286
  )
287
287
  ]
288
288
 
289
+ def __init__(self):
290
+ super().__init__()
291
+
289
292
  def load_yaml_file(self, file_path):
290
293
  """Load YAML file and return parsed content"""
291
294
  try:
@@ -327,13 +330,13 @@ contexts:
327
330
  query = "{ stack(id: \"" + stack_id + "\") { trackedCommit { hash } } }"
328
331
  response = self.query_api(query)
329
332
  if "errors" in response:
330
- self.logger.error("Error fetching stack tracked commit:", response["errors"])
333
+ self.logger.error(f"Error fetching stack tracked commit: {response['errors']}")
331
334
  continue
332
335
 
333
336
  # Ensure we have a tracked commit
334
337
  try:
335
338
  tracked_commit = response["data"]["stack"]["trackedCommit"]["hash"]
336
- except TypeError:
339
+ except (TypeError, KeyError):
337
340
  tracked_commit = None
338
341
  if tracked_commit is None:
339
342
  self.logger.error(f"Stack {stack_id} has no tracked commit. Skipping.")
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.0.7'
32
- __version_tuple__ = version_tuple = (0, 0, 7)
31
+ __version__ = version = '0.0.9'
32
+ __version_tuple__ = version_tuple = (0, 0, 9)
33
33
 
34
- __commit_id__ = commit_id = 'g0a9b4ad4a'
34
+ __commit_id__ = commit_id = 'geefe51823'
@@ -10,6 +10,7 @@ import subprocess
10
10
  import urllib.request
11
11
  from abc import ABC
12
12
  from typing import Any, Dict, List, Optional, Tuple
13
+ from urllib.error import HTTPError
13
14
 
14
15
 
15
16
  class SpaceforgePlugin(ABC):
@@ -48,14 +49,14 @@ class SpaceforgePlugin(ABC):
48
49
  if self.spacelift_domain and isinstance(self.spacelift_domain, str):
49
50
  # this must occur after we check if spacelift domain is false
50
51
  # because the domain could be set but not start with https://
51
- if self.spacelift_domain.startswith("https://"):
52
- if self.spacelift_domain.endswith("/"):
53
- self.spacelift_domain = self.spacelift_domain[:-1]
54
- else:
55
- self.logger.warning(
56
- "SPACELIFT_DOMAIN does not start with https://, api calls will fail."
57
- )
58
- self._api_enabled = False
52
+ # if self.spacelift_domain.startswith("https://"):
53
+ # if self.spacelift_domain.endswith("/"):
54
+ # self.spacelift_domain = self.spacelift_domain[:-1]
55
+ # else:
56
+ # self.logger.warning(
57
+ # "SPACELIFT_DOMAIN does not start with https://, api calls will fail."
58
+ # )
59
+ # self._api_enabled = False
59
60
 
60
61
  if self._api_enabled:
61
62
  self._spacelift_markdown_endpoint = self.spacelift_domain.replace(
@@ -204,8 +205,17 @@ class SpaceforgePlugin(ABC):
204
205
  json.dumps(data).encode("utf-8"),
205
206
  headers,
206
207
  )
207
- with urllib.request.urlopen(req) as response:
208
- resp: Dict[str, Any] = json.loads(response.read().decode("utf-8"))
208
+
209
+ try:
210
+ with urllib.request.urlopen(req) as response:
211
+ resp: Dict[str, Any] = json.loads(response.read().decode("utf-8"))
212
+ except urllib.error.HTTPError as e:
213
+ if hasattr(e, "read"):
214
+ resp = json.loads(e.read().decode("utf-8"))
215
+ else:
216
+ # We should not get here, but if we do re-raise the exception
217
+ self.logger.error(f"HTTP error occurred: ({e.code}) {e.reason} {e.msg}")
218
+ raise e
209
219
 
210
220
  if "errors" in resp:
211
221
  self.logger.error(f"Error: {resp['errors']}")
@@ -287,8 +297,8 @@ class SpaceforgePlugin(ABC):
287
297
  headers = resp["headers"]
288
298
  headers["Content-Type"] = "text/markdown"
289
299
  headers["Content-Length"] = str(len(markdown))
290
- except Exception as e:
291
- self.logger.error(f"HTTP error occurred: {e}")
300
+ except HTTPError as e:
301
+ self.logger.error(f"HTTP error occurred: ({e.code}) {e.reason} {e.msg}")
292
302
  return False
293
303
 
294
304
  # Now we upload the markdown content to the signed URL
@@ -307,8 +317,10 @@ class SpaceforgePlugin(ABC):
307
317
  )
308
318
  return False
309
319
  self.logger.debug("Markdown content uploaded successfully.")
310
- except Exception as e:
311
- self.logger.error(f"HTTP error occurred during upload: {e}")
320
+ except HTTPError as e:
321
+ self.logger.error(
322
+ f"HTTP error occurred during upload: ({e.code}) {e.reason} {e.msg}"
323
+ )
312
324
  return False
313
325
 
314
326
  return True
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spaceforge
3
- Version: 0.0.7
3
+ Version: 0.0.9
4
4
  Summary: A Python framework for building Spacelift plugins
5
5
  Home-page: https://github.com/spacelift-io/plugins
6
6
  Author: Spacelift
@@ -1,357 +0,0 @@
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes