re-uploading work
This commit is contained in:
560
packs/core/tests/test_actions.py
Executable file
560
packs/core/tests/test_actions.py
Executable file
@@ -0,0 +1,560 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit tests for Core Pack Actions
|
||||
|
||||
This test suite validates all core pack actions to ensure they behave correctly
|
||||
with various inputs, handle errors appropriately, and produce expected outputs.
|
||||
|
||||
Usage:
|
||||
python3 test_actions.py
|
||||
python3 -m pytest test_actions.py -v
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class CorePackTestCase(unittest.TestCase):
|
||||
"""Base test case for core pack tests"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up test environment"""
|
||||
# Get the actions directory
|
||||
cls.test_dir = Path(__file__).parent
|
||||
cls.pack_dir = cls.test_dir.parent
|
||||
cls.actions_dir = cls.pack_dir / "actions"
|
||||
|
||||
# Verify actions directory exists
|
||||
if not cls.actions_dir.exists():
|
||||
raise RuntimeError(f"Actions directory not found: {cls.actions_dir}")
|
||||
|
||||
# Check for required executables
|
||||
cls.has_python = cls._check_command("python3")
|
||||
cls.has_bash = cls._check_command("bash")
|
||||
|
||||
@staticmethod
|
||||
def _check_command(command):
|
||||
"""Check if a command is available"""
|
||||
try:
|
||||
subprocess.run(
|
||||
[command, "--version"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=2,
|
||||
)
|
||||
return True
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def run_action(self, script_name, env_vars=None, expect_failure=False):
|
||||
"""
|
||||
Run an action script with environment variables
|
||||
|
||||
Args:
|
||||
script_name: Name of the script file
|
||||
env_vars: Dictionary of environment variables
|
||||
expect_failure: If True, expects the script to fail
|
||||
|
||||
Returns:
|
||||
tuple: (stdout, stderr, exit_code)
|
||||
"""
|
||||
script_path = self.actions_dir / script_name
|
||||
if not script_path.exists():
|
||||
raise FileNotFoundError(f"Script not found: {script_path}")
|
||||
|
||||
# Prepare environment
|
||||
env = os.environ.copy()
|
||||
if env_vars:
|
||||
env.update(env_vars)
|
||||
|
||||
# Determine the command
|
||||
if script_name.endswith(".py"):
|
||||
cmd = ["python3", str(script_path)]
|
||||
elif script_name.endswith(".sh"):
|
||||
cmd = ["bash", str(script_path)]
|
||||
else:
|
||||
raise ValueError(f"Unknown script type: {script_name}")
|
||||
|
||||
# Run the script
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=10,
|
||||
cwd=str(self.actions_dir),
|
||||
)
|
||||
return (
|
||||
result.stdout.decode("utf-8"),
|
||||
result.stderr.decode("utf-8"),
|
||||
result.returncode,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
if expect_failure:
|
||||
return "", "Timeout", -1
|
||||
raise
|
||||
|
||||
|
||||
class TestEchoAction(CorePackTestCase):
|
||||
"""Tests for core.echo action"""
|
||||
|
||||
def test_basic_echo(self):
|
||||
"""Test basic echo functionality"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"echo.sh", {"ATTUNE_ACTION_MESSAGE": "Hello, Attune!"}
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("Hello, Attune!", stdout)
|
||||
|
||||
def test_default_message(self):
|
||||
"""Test default message when none provided"""
|
||||
stdout, stderr, code = self.run_action("echo.sh", {})
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("Hello, World!", stdout)
|
||||
|
||||
def test_uppercase_conversion(self):
|
||||
"""Test uppercase conversion"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"echo.sh",
|
||||
{
|
||||
"ATTUNE_ACTION_MESSAGE": "test message",
|
||||
"ATTUNE_ACTION_UPPERCASE": "true",
|
||||
},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("TEST MESSAGE", stdout)
|
||||
self.assertNotIn("test message", stdout)
|
||||
|
||||
def test_uppercase_false(self):
|
||||
"""Test uppercase=false preserves case"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"echo.sh",
|
||||
{
|
||||
"ATTUNE_ACTION_MESSAGE": "Mixed Case",
|
||||
"ATTUNE_ACTION_UPPERCASE": "false",
|
||||
},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("Mixed Case", stdout)
|
||||
|
||||
def test_empty_message(self):
|
||||
"""Test empty message"""
|
||||
stdout, stderr, code = self.run_action("echo.sh", {"ATTUNE_ACTION_MESSAGE": ""})
|
||||
self.assertEqual(code, 0)
|
||||
# Empty message should produce a newline
|
||||
# bash echo with empty string still outputs newline
|
||||
|
||||
def test_special_characters(self):
|
||||
"""Test message with special characters"""
|
||||
special_msg = "Test!@#$%^&*()[]{}|\\:;\"'<>,.?/~`"
|
||||
stdout, stderr, code = self.run_action(
|
||||
"echo.sh", {"ATTUNE_ACTION_MESSAGE": special_msg}
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn(special_msg, stdout)
|
||||
|
||||
def test_multiline_message(self):
|
||||
"""Test message with newlines"""
|
||||
multiline_msg = "Line 1\nLine 2\nLine 3"
|
||||
stdout, stderr, code = self.run_action(
|
||||
"echo.sh", {"ATTUNE_ACTION_MESSAGE": multiline_msg}
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
# Depending on shell behavior, newlines might be interpreted
|
||||
|
||||
|
||||
class TestNoopAction(CorePackTestCase):
|
||||
"""Tests for core.noop action"""
|
||||
|
||||
def test_basic_noop(self):
|
||||
"""Test basic noop functionality"""
|
||||
stdout, stderr, code = self.run_action("noop.sh", {})
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("No operation completed successfully", stdout)
|
||||
|
||||
def test_noop_with_message(self):
|
||||
"""Test noop with custom message"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"noop.sh", {"ATTUNE_ACTION_MESSAGE": "Test message"}
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("Test message", stdout)
|
||||
self.assertIn("No operation completed successfully", stdout)
|
||||
|
||||
def test_custom_exit_code_success(self):
|
||||
"""Test custom exit code 0"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "0"}
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
def test_custom_exit_code_failure(self):
|
||||
"""Test custom exit code non-zero"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "5"}
|
||||
)
|
||||
self.assertEqual(code, 5)
|
||||
|
||||
def test_custom_exit_code_max(self):
|
||||
"""Test maximum valid exit code (255)"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "255"}
|
||||
)
|
||||
self.assertEqual(code, 255)
|
||||
|
||||
def test_invalid_negative_exit_code(self):
|
||||
"""Test that negative exit codes are rejected"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "-1"}, expect_failure=True
|
||||
)
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertIn("ERROR", stderr)
|
||||
|
||||
def test_invalid_large_exit_code(self):
|
||||
"""Test that exit codes > 255 are rejected"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "999"}, expect_failure=True
|
||||
)
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertIn("ERROR", stderr)
|
||||
|
||||
def test_invalid_non_numeric_exit_code(self):
|
||||
"""Test that non-numeric exit codes are rejected"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"noop.sh", {"ATTUNE_ACTION_EXIT_CODE": "abc"}, expect_failure=True
|
||||
)
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertIn("ERROR", stderr)
|
||||
|
||||
|
||||
class TestSleepAction(CorePackTestCase):
|
||||
"""Tests for core.sleep action"""
|
||||
|
||||
def test_basic_sleep(self):
|
||||
"""Test basic sleep functionality"""
|
||||
start = time.time()
|
||||
stdout, stderr, code = self.run_action(
|
||||
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "1"}
|
||||
)
|
||||
elapsed = time.time() - start
|
||||
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("Slept for 1 seconds", stdout)
|
||||
self.assertGreaterEqual(elapsed, 1.0)
|
||||
self.assertLess(elapsed, 1.5) # Should not take too long
|
||||
|
||||
def test_zero_seconds(self):
|
||||
"""Test sleep with 0 seconds"""
|
||||
start = time.time()
|
||||
stdout, stderr, code = self.run_action(
|
||||
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "0"}
|
||||
)
|
||||
elapsed = time.time() - start
|
||||
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("Slept for 0 seconds", stdout)
|
||||
self.assertLess(elapsed, 0.5)
|
||||
|
||||
def test_sleep_with_message(self):
|
||||
"""Test sleep with custom message"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"sleep.sh",
|
||||
{"ATTUNE_ACTION_SECONDS": "1", "ATTUNE_ACTION_MESSAGE": "Sleeping now..."},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("Sleeping now...", stdout)
|
||||
self.assertIn("Slept for 1 seconds", stdout)
|
||||
|
||||
def test_default_sleep_duration(self):
|
||||
"""Test default sleep duration (1 second)"""
|
||||
start = time.time()
|
||||
stdout, stderr, code = self.run_action("sleep.sh", {})
|
||||
elapsed = time.time() - start
|
||||
|
||||
self.assertEqual(code, 0)
|
||||
self.assertGreaterEqual(elapsed, 1.0)
|
||||
|
||||
def test_invalid_negative_seconds(self):
|
||||
"""Test that negative seconds are rejected"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "-1"}, expect_failure=True
|
||||
)
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertIn("ERROR", stderr)
|
||||
|
||||
def test_invalid_large_seconds(self):
|
||||
"""Test that seconds > 3600 are rejected"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "9999"}, expect_failure=True
|
||||
)
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertIn("ERROR", stderr)
|
||||
|
||||
def test_invalid_non_numeric_seconds(self):
|
||||
"""Test that non-numeric seconds are rejected"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "abc"}, expect_failure=True
|
||||
)
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertIn("ERROR", stderr)
|
||||
|
||||
def test_multi_second_sleep(self):
|
||||
"""Test sleep with multiple seconds"""
|
||||
start = time.time()
|
||||
stdout, stderr, code = self.run_action(
|
||||
"sleep.sh", {"ATTUNE_ACTION_SECONDS": "2"}
|
||||
)
|
||||
elapsed = time.time() - start
|
||||
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("Slept for 2 seconds", stdout)
|
||||
self.assertGreaterEqual(elapsed, 2.0)
|
||||
self.assertLess(elapsed, 2.5)
|
||||
|
||||
|
||||
class TestHttpRequestAction(CorePackTestCase):
|
||||
"""Tests for core.http_request action"""
|
||||
|
||||
def setUp(self):
|
||||
"""Check if we can run HTTP tests"""
|
||||
if not self.has_python:
|
||||
self.skipTest("Python3 not available")
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
self.skipTest("requests library not installed")
|
||||
|
||||
def test_simple_get_request(self):
|
||||
"""Test simple GET request"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py",
|
||||
{
|
||||
"ATTUNE_ACTION_URL": "https://httpbin.org/get",
|
||||
"ATTUNE_ACTION_METHOD": "GET",
|
||||
},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
# Parse JSON output
|
||||
result = json.loads(stdout)
|
||||
self.assertEqual(result["status_code"], 200)
|
||||
self.assertTrue(result["success"])
|
||||
self.assertIn("httpbin.org", result["url"])
|
||||
|
||||
def test_missing_url_parameter(self):
|
||||
"""Test that missing URL parameter causes failure"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py", {}, expect_failure=True
|
||||
)
|
||||
self.assertNotEqual(code, 0)
|
||||
self.assertIn("Required parameter 'url' not provided", stderr)
|
||||
|
||||
def test_post_with_json(self):
|
||||
"""Test POST request with JSON body"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py",
|
||||
{
|
||||
"ATTUNE_ACTION_URL": "https://httpbin.org/post",
|
||||
"ATTUNE_ACTION_METHOD": "POST",
|
||||
"ATTUNE_ACTION_JSON_BODY": '{"test": "value", "number": 123}',
|
||||
},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
result = json.loads(stdout)
|
||||
self.assertEqual(result["status_code"], 200)
|
||||
self.assertTrue(result["success"])
|
||||
# Check that our data was echoed back
|
||||
self.assertIsNotNone(result.get("json"))
|
||||
# httpbin.org echoes data in different format, just verify JSON was sent
|
||||
body_json = json.loads(result["body"])
|
||||
self.assertIn("json", body_json)
|
||||
self.assertEqual(body_json["json"]["test"], "value")
|
||||
|
||||
def test_custom_headers(self):
|
||||
"""Test request with custom headers"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py",
|
||||
{
|
||||
"ATTUNE_ACTION_URL": "https://httpbin.org/headers",
|
||||
"ATTUNE_ACTION_METHOD": "GET",
|
||||
"ATTUNE_ACTION_HEADERS": '{"X-Custom-Header": "test-value"}',
|
||||
},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
result = json.loads(stdout)
|
||||
self.assertEqual(result["status_code"], 200)
|
||||
# The response body should contain our custom header
|
||||
body_data = json.loads(result["body"])
|
||||
self.assertIn("X-Custom-Header", body_data["headers"])
|
||||
|
||||
def test_query_parameters(self):
|
||||
"""Test request with query parameters"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py",
|
||||
{
|
||||
"ATTUNE_ACTION_URL": "https://httpbin.org/get",
|
||||
"ATTUNE_ACTION_METHOD": "GET",
|
||||
"ATTUNE_ACTION_QUERY_PARAMS": '{"foo": "bar", "page": "1"}',
|
||||
},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
result = json.loads(stdout)
|
||||
self.assertEqual(result["status_code"], 200)
|
||||
# Check query params in response
|
||||
body_data = json.loads(result["body"])
|
||||
self.assertEqual(body_data["args"]["foo"], "bar")
|
||||
self.assertEqual(body_data["args"]["page"], "1")
|
||||
|
||||
def test_timeout_handling(self):
|
||||
"""Test request timeout"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py",
|
||||
{
|
||||
"ATTUNE_ACTION_URL": "https://httpbin.org/delay/10",
|
||||
"ATTUNE_ACTION_METHOD": "GET",
|
||||
"ATTUNE_ACTION_TIMEOUT": "2",
|
||||
},
|
||||
expect_failure=True,
|
||||
)
|
||||
# Should fail due to timeout
|
||||
self.assertNotEqual(code, 0)
|
||||
|
||||
result = json.loads(stdout)
|
||||
self.assertFalse(result["success"])
|
||||
self.assertIn("error", result)
|
||||
|
||||
def test_404_status_code(self):
|
||||
"""Test handling of 404 status"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py",
|
||||
{
|
||||
"ATTUNE_ACTION_URL": "https://httpbin.org/status/404",
|
||||
"ATTUNE_ACTION_METHOD": "GET",
|
||||
},
|
||||
expect_failure=True,
|
||||
)
|
||||
# Non-2xx status codes should fail
|
||||
self.assertNotEqual(code, 0)
|
||||
|
||||
result = json.loads(stdout)
|
||||
self.assertEqual(result["status_code"], 404)
|
||||
self.assertFalse(result["success"])
|
||||
|
||||
def test_different_methods(self):
|
||||
"""Test different HTTP methods"""
|
||||
methods = ["PUT", "PATCH", "DELETE"]
|
||||
|
||||
for method in methods:
|
||||
with self.subTest(method=method):
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py",
|
||||
{
|
||||
"ATTUNE_ACTION_URL": f"https://httpbin.org/{method.lower()}",
|
||||
"ATTUNE_ACTION_METHOD": method,
|
||||
},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
result = json.loads(stdout)
|
||||
self.assertEqual(result["status_code"], 200)
|
||||
|
||||
def test_elapsed_time_reported(self):
|
||||
"""Test that elapsed time is reported"""
|
||||
stdout, stderr, code = self.run_action(
|
||||
"http_request.py",
|
||||
{
|
||||
"ATTUNE_ACTION_URL": "https://httpbin.org/get",
|
||||
"ATTUNE_ACTION_METHOD": "GET",
|
||||
},
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
result = json.loads(stdout)
|
||||
self.assertIn("elapsed_ms", result)
|
||||
self.assertIsInstance(result["elapsed_ms"], int)
|
||||
self.assertGreater(result["elapsed_ms"], 0)
|
||||
|
||||
|
||||
class TestFilePermissions(CorePackTestCase):
|
||||
"""Test that action scripts have correct permissions"""
|
||||
|
||||
def test_echo_executable(self):
|
||||
"""Test that echo.sh is executable"""
|
||||
script_path = self.actions_dir / "echo.sh"
|
||||
self.assertTrue(os.access(script_path, os.X_OK))
|
||||
|
||||
def test_noop_executable(self):
|
||||
"""Test that noop.sh is executable"""
|
||||
script_path = self.actions_dir / "noop.sh"
|
||||
self.assertTrue(os.access(script_path, os.X_OK))
|
||||
|
||||
def test_sleep_executable(self):
|
||||
"""Test that sleep.sh is executable"""
|
||||
script_path = self.actions_dir / "sleep.sh"
|
||||
self.assertTrue(os.access(script_path, os.X_OK))
|
||||
|
||||
def test_http_request_executable(self):
|
||||
"""Test that http_request.py is executable"""
|
||||
script_path = self.actions_dir / "http_request.py"
|
||||
self.assertTrue(os.access(script_path, os.X_OK))
|
||||
|
||||
|
||||
class TestYAMLSchemas(CorePackTestCase):
|
||||
"""Test that YAML schemas are valid"""
|
||||
|
||||
def test_pack_yaml_valid(self):
|
||||
"""Test that pack.yaml is valid YAML"""
|
||||
pack_yaml = self.pack_dir / "pack.yaml"
|
||||
try:
|
||||
import yaml
|
||||
|
||||
with open(pack_yaml) as f:
|
||||
data = yaml.safe_load(f)
|
||||
self.assertIsNotNone(data)
|
||||
self.assertIn("ref", data)
|
||||
self.assertEqual(data["ref"], "core")
|
||||
except ImportError:
|
||||
self.skipTest("PyYAML not installed")
|
||||
|
||||
def test_action_yamls_valid(self):
|
||||
"""Test that all action YAML files are valid"""
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
self.skipTest("PyYAML not installed")
|
||||
|
||||
for yaml_file in (self.actions_dir).glob("*.yaml"):
|
||||
with self.subTest(file=yaml_file.name):
|
||||
with open(yaml_file) as f:
|
||||
data = yaml.safe_load(f)
|
||||
self.assertIsNotNone(data)
|
||||
self.assertIn("name", data)
|
||||
self.assertIn("ref", data)
|
||||
self.assertIn("runner_type", data)
|
||||
|
||||
|
||||
def main():
|
||||
"""Run tests"""
|
||||
# Check for pytest
|
||||
try:
|
||||
import pytest
|
||||
|
||||
# Run with pytest if available
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
except ImportError:
|
||||
# Fall back to unittest
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user