//! Integration tests for CLI action commands #![allow(deprecated)] use assert_cmd::Command; use predicates::prelude::*; use serde_json::json; use wiremock::{ matchers::{method, path}, Mock, ResponseTemplate, }; mod common; use common::*; #[tokio::test] async fn test_action_list_authenticated() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action list endpoint mock_action_list(&fixture.mock_server).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("list"); cmd.assert() .success() .stdout(predicate::str::contains("core.echo")) .stdout(predicate::str::contains("Echo a message")); } #[tokio::test] async fn test_action_list_unauthenticated() { let fixture = TestFixture::new().await; fixture.write_default_config(); // Mock unauthorized response mock_unauthorized(&fixture.mock_server, "/api/v1/actions").await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("list"); cmd.assert().failure(); } #[tokio::test] async fn test_action_list_json_output() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action list endpoint mock_action_list(&fixture.mock_server).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("--json") .arg("action") .arg("list"); cmd.assert() .success() .stdout(predicate::str::contains(r#""ref""#)) .stdout(predicate::str::contains(r#"core.echo"#)); } #[tokio::test] async fn test_action_list_yaml_output() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action list endpoint mock_action_list(&fixture.mock_server).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("--yaml") .arg("action") .arg("list"); cmd.assert() .success() .stdout(predicate::str::contains("core.echo")) .stdout(predicate::str::contains("Echo a message")); } #[tokio::test] async fn test_action_get_by_ref() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action get endpoint Mock::given(method("GET")) .and(path("/api/v1/actions/core.echo")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "data": { "id": 1, "ref": "core.echo", "pack": 1, "pack_ref": "core", "label": "Echo Action", "description": "Echo a message", "entrypoint": "echo.py", "runtime": null, "param_schema": { "message": { "type": "string", "description": "Message to echo", "required": true } }, "out_schema": null, "created": "2024-01-01T00:00:00Z", "updated": "2024-01-01T00:00:00Z" } }))) .mount(&fixture.mock_server) .await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("show") .arg("core.echo"); cmd.assert() .success() .stdout(predicate::str::contains("core.echo")) .stdout(predicate::str::contains("Echo a message")); } #[tokio::test] async fn test_action_get_not_found() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock 404 response mock_not_found(&fixture.mock_server, "/api/v1/actions/nonexistent.action").await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("show") .arg("nonexistent.action"); cmd.assert() .failure() .stderr(predicate::str::contains("Error")); } #[tokio::test] async fn test_action_execute_with_parameters() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action execute endpoint mock_action_execute(&fixture.mock_server, 42).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("execute") .arg("core.echo") .arg("--param") .arg("message=Hello World"); cmd.assert() .success() .stdout(predicate::str::contains("42").or(predicate::str::contains("scheduled"))); } #[tokio::test] async fn test_action_execute_multiple_parameters() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action execute endpoint mock_action_execute(&fixture.mock_server, 100).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("execute") .arg("linux.run_command") .arg("--param") .arg("cmd=ls -la") .arg("--param") .arg("timeout=30"); cmd.assert().success(); } #[tokio::test] async fn test_action_execute_with_json_parameters() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action execute endpoint mock_action_execute(&fixture.mock_server, 101).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("execute") .arg("core.webhook") .arg("--params-json") .arg(r#"{"url": "https://example.com", "method": "POST"}"#); cmd.assert().success(); } #[tokio::test] async fn test_action_execute_without_parameters() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action execute endpoint mock_action_execute(&fixture.mock_server, 200).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("execute") .arg("core.no_params_action"); cmd.assert().success(); } #[tokio::test] async fn test_action_execute_json_output() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action execute endpoint mock_action_execute(&fixture.mock_server, 150).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("--json") .arg("action") .arg("execute") .arg("core.echo") .arg("--param") .arg("message=test"); cmd.assert() .success() .stdout(predicate::str::contains("150")) .stdout(predicate::str::contains("scheduled")); } #[tokio::test] async fn test_action_execute_wait_for_completion() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action execute endpoint mock_action_execute(&fixture.mock_server, 250).await; // Mock execution polling - first running, then succeeded Mock::given(method("GET")) .and(path("/api/v1/executions/250")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "data": { "id": 250, "action": 1, "action_ref": "core.echo", "config": {"message": "test"}, "parent": null, "enforcement": null, "executor": null, "status": "succeeded", "result": {"output": "test"}, "created": "2024-01-01T00:00:00Z", "updated": "2024-01-01T00:00:00Z" } }))) .mount(&fixture.mock_server) .await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("execute") .arg("core.echo") .arg("--param") .arg("message=test") .arg("--wait"); cmd.assert() .success() .stdout(predicate::str::contains("succeeded")); } #[tokio::test] #[ignore = "Profile switching needs more investigation - CLI integration issue"] async fn test_action_execute_with_profile() { let fixture = TestFixture::new().await; // Create multi-profile config let config = format!( r#" current_profile: default default_output_format: table profiles: default: api_url: {} auth_token: default_token refresh_token: default_refresh production: api_url: {} auth_token: prod_token refresh_token: prod_refresh "#, fixture.server_url(), fixture.server_url() ); fixture.write_config(&config); // Mock action execute endpoint mock_action_execute(&fixture.mock_server, 300).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--profile") .arg("production") .arg("action") .arg("execute") .arg("core.echo") .arg("--param") .arg("message=prod_test"); cmd.assert().success(); } #[tokio::test] async fn test_action_execute_invalid_param_format() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("execute") .arg("core.echo") .arg("--param") .arg("invalid_format_no_equals"); cmd.assert() .failure() .stderr(predicate::str::contains("Error").or(predicate::str::contains("="))); } #[tokio::test] async fn test_action_execute_invalid_json_parameters() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("execute") .arg("core.echo") .arg("--params-json") .arg(r#"{"invalid json"#); cmd.assert() .failure() .stderr(predicate::str::contains("Error").or(predicate::str::contains("JSON"))); } #[tokio::test] async fn test_action_list_by_pack() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action list for a specific pack Mock::given(method("GET")) .and(path("/api/v1/packs/core/actions")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "data": [ { "id": 1, "ref": "core.echo", "pack_ref": "core", "label": "Echo Action", "description": "Echo a message", "entrypoint": "echo.py", "runtime": null, "created": "2024-01-01T00:00:00Z", "updated": "2024-01-01T00:00:00Z" } ], "meta": { "page": 1, "limit": 50, "total": 1, "total_pages": 1 } }))) .mount(&fixture.mock_server) .await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("list") .arg("--pack") .arg("core"); cmd.assert().success(); } #[tokio::test] async fn test_action_execute_async_flag() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action execute endpoint mock_action_execute(&fixture.mock_server, 400).await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("execute") .arg("core.long_running"); // Note: default behavior is async (no --wait), so no --async flag needed cmd.assert() .success() .stdout(predicate::str::contains("scheduled").or(predicate::str::contains("400"))); } #[tokio::test] async fn test_action_list_empty_result() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock empty action list Mock::given(method("GET")) .and(path("/api/v1/actions")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "data": [] }))) .mount(&fixture.mock_server) .await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("list"); cmd.assert().success(); } #[tokio::test] async fn test_action_get_shows_parameters() { let fixture = TestFixture::new().await; fixture.write_authenticated_config("valid_token", "refresh_token"); // Mock action get with detailed parameters Mock::given(method("GET")) .and(path("/api/v1/actions/core.complex")) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ "data": { "id": 5, "ref": "core.complex", "pack": 1, "pack_ref": "core", "label": "Complex Action", "description": "Complex action with multiple params", "entrypoint": "complex.py", "runtime": null, "param_schema": { "required_string": { "type": "string", "description": "A required string parameter", "required": true }, "optional_number": { "type": "integer", "description": "An optional number", "required": false, "default": 42 }, "boolean_flag": { "type": "boolean", "description": "A boolean flag", "required": false, "default": false } }, "out_schema": null, "created": "2024-01-01T00:00:00Z", "updated": "2024-01-01T00:00:00Z" } }))) .mount(&fixture.mock_server) .await; let mut cmd = Command::cargo_bin("attune").unwrap(); cmd.env("XDG_CONFIG_HOME", fixture.config_dir_path()) .env("HOME", fixture.config_dir_path()) .arg("--api-url") .arg(fixture.server_url()) .arg("action") .arg("show") .arg("core.complex"); cmd.assert() .success() .stdout(predicate::str::contains("required_string")) .stdout(predicate::str::contains("optional_number")); }